-
-
Save flexelektro/47ce4e3d570302ea45352cdabd456473 to your computer and use it in GitHub Desktop.
WebGL Raymarcher
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<!-- Thanks to Inigo Quillez (http://iquilezles.org) for many of the functions in the shader code. --> | |
<!-- Thanks to MDN for being awesome. --> | |
<!-- Thanks to WebGLFundamentals (https://webglfundamentals.org) for their framebuffer tutorial. --> | |
<!-- Thanks to gl-matrix.js for the matrix library, and Stats.js for the FPS counter. --> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Raymarcher</title> | |
<style media="screen"> | |
html, body { | |
margin: 0; | |
padding: 0; | |
overflow: hidden; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas></canvas> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.7.1/gl-matrix-min.js" integrity="sha256-+09xst+d1zIS41eAvRDCXOf0MH993E4cS40hKBIJj8Q=" crossorigin="anonymous"></script> | |
<script src="https://cdn.rawgit.com/mrdoob/stats.js/r16/src/Stats.js"></script> | |
<script type="text/vnd.glsl-shader" id="copyShader"> | |
varying highp vec2 pos; | |
[vertex] | |
attribute vec4 vertexPosition; | |
void main() { | |
gl_Position = vertexPosition; | |
pos = vertexPosition.xy; | |
} | |
[fragment] | |
uniform sampler2D depthSoFar; | |
void main() { | |
gl_FragColor = texture2D(depthSoFar, (pos + vec2(1.0)) / 2.0) * vec4(1.0, 1.0, 1.0, 1.0); | |
} | |
</script> | |
<script type="text/vnd.glsl-shader" id="scene"> | |
uniform mediump float time; | |
mediump float sphereShape(vec3 p, float s) | |
{ | |
return length(p) - s; | |
} | |
mediump float boxShape(vec3 p, vec3 b) | |
{ | |
mediump vec3 d = abs(p) - b; | |
return min(max(d.x,max(d.y,d.z)),0.0) + length(max(d,0.0)); | |
} | |
mediump float crossShape(vec3 p, vec3 b) | |
{ | |
return min(boxShape(p.xyz,vec3(1000.0,b.yz)),min(boxShape(p.yzx,vec3(b.x,1000.0,b.z)),boxShape(p.zxy,vec3(b.xy,1000.0)))); | |
} | |
mediump float smin( float a, float b, float k ) | |
{ | |
mediump float h = clamp( 0.5+0.5*(b-a)/k, 0.0, 1.0 ); | |
return mix( b, a, h ) - k*h*(1.0-h); | |
} | |
mediump float scene(vec3 p) | |
{ | |
mediump vec3 rep = vec3(7.0, 7.0, 7.0); | |
mediump vec3 p_rep = mod(p, rep) - rep * 0.5; | |
mediump vec3 p_c = p * 2.0 + vec3(0.67, 0.76, 0.23) * time; | |
return sphereShape(p_rep, (sin(p_c.x) * sin(p_c.y) * sin(p_c.z) + 3.0) * 0.5); | |
} | |
mediump vec3 sceneNormal(vec3 p, float eps) | |
{ | |
mediump float o = scene(p); | |
return normalize(vec3( | |
o - scene(p + vec3(eps, 0, 0)), | |
o - scene(p + vec3(0, eps, 0)), | |
o - scene(p + vec3(0, 0, eps)) | |
)); | |
// return normalize(vec3( | |
// scene(p - vec3(eps, 0, 0)) - scene(p + vec3(eps, 0, 0)), | |
// scene(p - vec3(eps, 0, 0)) - scene(p + vec3(0, eps, 0)), | |
// scene(p - vec3(eps, 0, 0)) - scene(p + vec3(0, 0, eps)) | |
// )); | |
} | |
</script> | |
<script type="text/vnd.glsl-shader" id="depthShader"> | |
varying highp vec2 pos; | |
[vertex] | |
attribute vec4 vertexPosition; | |
void main() { | |
gl_Position = vertexPosition; | |
pos = vertexPosition.xy; | |
} | |
[fragment include:#scene] | |
uniform mediump mat4 projectionMatrix; | |
uniform mediump mat4 modelViewMatrix; | |
uniform mediump vec2 pixelSize; | |
uniform mediump float far; | |
uniform sampler2D depthSoFar; | |
highp vec3 computeRayDirection(vec2 screenPosition) { | |
return normalize(vec3(screenPosition.x / projectionMatrix[0].x, screenPosition.y / projectionMatrix[1].y, 1)); | |
} | |
void main() { | |
mediump vec3 rayDirection = computeRayDirection(pos); | |
mediump vec3 nextRayDirection = computeRayDirection(pos + pixelSize * 3.0); | |
mediump float raySlope = distance(rayDirection, nextRayDirection); | |
mediump vec3 rayOrigin = projectionMatrix[3].xyz; | |
mediump vec4 soFar = texture2D(depthSoFar, (pos + vec2(1.0)) / 2.0); | |
mediump float depth = soFar.x * far; | |
if (depth > far) { | |
gl_FragColor = vec4(soFar.x, soFar.x, soFar.z / 4.0, 1.0); | |
return; | |
} | |
mediump float distance = 0.0; | |
int iterations = 1; | |
depth -= abs(scene((modelViewMatrix * vec4(rayOrigin + depth * rayDirection, 1.0)).xyz)); | |
for (int i = 0; i < 64; i++) { | |
distance = scene((modelViewMatrix * vec4(rayOrigin + depth * rayDirection, 1.0)).xyz); | |
if (distance < raySlope * depth || depth > far) { | |
break; | |
} | |
depth += distance * 0.7; | |
iterations += 1; | |
} | |
gl_FragColor = vec4(depth / far, soFar.x, soFar.z / 4.0 + float(iterations) / 64.0, 1.0); | |
} | |
</script> | |
<script type="text/vnd.glsl-shader" id="shadingShader"> | |
varying highp vec2 pos; | |
[vertex] | |
attribute vec4 vertexPosition; | |
void main() { | |
gl_Position = vertexPosition; | |
pos = vertexPosition.xy; | |
} | |
[fragment include:#scene] | |
uniform mediump mat4 projectionMatrix; | |
uniform mediump mat4 modelViewMatrix; | |
uniform mediump float far; | |
uniform sampler2D depthSoFar; | |
void main() { | |
mediump vec3 rayDirection = normalize(vec3(pos.x / projectionMatrix[0].x, pos.y / projectionMatrix[1].y, 1)); | |
mediump vec3 rayOrigin = projectionMatrix[3].xyz; | |
mediump vec4 soFar = texture2D(depthSoFar, (pos + vec2(1.0)) / 2.0); | |
mediump float depth = soFar.x * far; | |
mediump vec3 normal = sceneNormal((modelViewMatrix * vec4(rayOrigin + depth * rayDirection, 1.0)).xyz, 0.01); | |
mediump vec3 light1Color = vec3(1.0, 1.0, 0.9) * 0.8; | |
mediump vec3 light1Direction = normalize(vec3(0.0, -1.0, 1.0)); | |
mediump vec3 light2Color = vec3(1.0, 1.0, 1.0) * 0.3; | |
mediump vec3 light2Direction = normalize(vec3(1.0, 1.0, 1.0)); | |
mediump vec3 light3Color = vec3(0.8, 0.8, 1.0) * 0.3; | |
mediump vec3 light3Direction = normalize(vec3(0.0, 1.0, 1.0)); | |
gl_FragColor = mix( | |
vec4( | |
light1Color * clamp(dot(normal, light1Direction), 0.0, 1.0) + | |
light2Color * clamp(dot(normal, light2Direction), 0.0, 1.0) + | |
light3Color * clamp(dot(normal, light3Direction), 0.0, 1.0), 1.0), | |
vec4(0.0, 0.0, 0.0, 1.0), | |
pow(depth / far, 2.0) | |
); | |
// gl_FragColor = vec4(0.0, 0.0, soFar.z, 1.0); | |
// gl_FragColor = vec4((soFar.x - soFar.y), 0.0, 0.0, soFar.w); | |
} | |
</script> | |
<script type="text/javascript"> | |
var stats = Stats(); | |
stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom | |
document.body.appendChild(stats.dom); | |
var canvas = document.querySelector('canvas'); | |
var gl = canvas.getContext('experimental-webgl') || canvas.getContext('webgl2') || canvas.getContext('webgl'); | |
if (!gl) { | |
alert('WebGL is unsupported on your browser!'); | |
throw new Error('WebGL is not supported!'); | |
} | |
gl.cbf = gl.getExtension('WEBGL_color_buffer_float') || gl.getExtension('EXT_color_buffer_float'); | |
gl.tf = gl.getExtension('OES_texture_float'); | |
var depthProgram = loadProgram('#depthShader', ['vertexPosition'], ['projectionMatrix', 'modelViewMatrix', 'depthSoFar', 'far', 'time', 'pixelSize']); | |
var shadingProgram = loadProgram('#shadingShader', ['vertexPosition'], ['projectionMatrix', 'modelViewMatrix', 'depthSoFar', 'far', 'time']); | |
var copyProgram = loadProgram('#copyShader', ['vertexPosition'], ['depthSoFar']); | |
var projectionMatrix = mat4.create(); | |
var modelViewMatrix = mat4.create(); | |
mat4.rotate(modelViewMatrix, modelViewMatrix, 22 * Math.PI / 180, [0.8, -1.0, 0.0]); | |
mat4.translate(modelViewMatrix, modelViewMatrix, [0.0, -0.0, -0.0]); | |
var rectCornersBuffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, rectCornersBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ | |
-1.0, +1.0, +0.0, +1.0, | |
+1.0, +1.0, +0.0, +1.0, | |
-1.0, -1.0, +0.0, +1.0, | |
+1.0, -1.0, +0.0, +1.0, | |
]), gl.STATIC_DRAW); | |
var blackTexture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, blackTexture); | |
{ | |
let level = 0; | |
let internalFormat = gl.RGB; | |
let width = 1; | |
let height = 1; | |
let border = 0; | |
let srcFormat = gl.RGB; | |
let srcType = gl.UNSIGNED_BYTE; | |
let pixel = new Uint8Array([0, 0, 0]); | |
gl.texImage2D( | |
gl.TEXTURE_2D, level, internalFormat, | |
width, height, border, srcFormat, srcType, | |
pixel | |
); | |
} | |
var framebuffers = []; | |
resize(); | |
window.addEventListener('resize', resize); | |
render(performance.now()); | |
function resize() { | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
mat4.perspective( | |
projectionMatrix, | |
45 * Math.PI / 180, // fov | |
canvas.clientWidth / canvas.clientHeight, // aspect | |
0.1, // near | |
100 // far | |
); | |
mat4.translate(projectionMatrix, projectionMatrix, [0.0, -0.0, 5.0]); | |
for (let framebuffer of framebuffers) { | |
gl.deleteFramebuffer(framebuffer); | |
} | |
framebuffers = []; | |
function createFramebuffer(width, height) { | |
let framebufferTexture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, framebufferTexture); | |
let internalFormat = gl.RGBA; | |
let border = 0; | |
let format = gl.RGBA; | |
let type = gl.tf ? gl.FLOAT : gl.UNSIGNED_BYTE; | |
gl.texImage2D( | |
gl.TEXTURE_2D, 0, internalFormat, | |
width, height, border, | |
format, type, null | |
); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
let framebuffer = gl.createFramebuffer(); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); | |
framebuffer.texture = framebufferTexture; | |
framebuffer.width = width; | |
framebuffer.height = height; | |
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, framebufferTexture, 0); | |
gl.bindTexture(gl.TEXTURE_2D, null); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
framebuffers.push(framebuffer); | |
} | |
for (let size = 16; size < canvas.width; size *= 2) { | |
createFramebuffer(size, Math.ceil(size * canvas.height / canvas.width)); | |
} | |
createFramebuffer(canvas.width, canvas.height); | |
// createFramebuffer(canvas.width, canvas.height); | |
// createFramebuffer(canvas.width, canvas.height); | |
} | |
function render(time) { | |
stats.begin(); | |
gl.useProgram(depthProgram); | |
gl.bindBuffer(gl.ARRAY_BUFFER, rectCornersBuffer); | |
gl.vertexAttribPointer(depthProgram.vertexPosition, 4, gl.FLOAT, false, 0, 0); | |
gl.enableVertexAttribArray(depthProgram.vertexPosition); | |
gl.uniformMatrix4fv(depthProgram.projectionMatrix, false, projectionMatrix); | |
gl.uniformMatrix4fv(depthProgram.modelViewMatrix, false, modelViewMatrix); | |
gl.uniform1f(depthProgram.far, 100.0); | |
gl.uniform1f(depthProgram.time, time / 1000.0); | |
gl.uniform1i(depthProgram.depthSoFar, 0); | |
gl.activeTexture(gl.TEXTURE0); | |
gl.bindTexture(gl.TEXTURE_2D, blackTexture); | |
for (let framebuffer of framebuffers) { | |
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); | |
gl.uniform2f(depthProgram.pixelSize, 1.00 / framebuffer.width, 1.00 / framebuffer.height); | |
gl.viewport(0, 0, framebuffer.width, framebuffer.height); | |
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
gl.bindTexture(gl.TEXTURE_2D, framebuffer.texture); | |
} | |
gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
gl.viewport(0, 0, canvas.width, canvas.height); | |
gl.useProgram(shadingProgram); | |
gl.bindBuffer(gl.ARRAY_BUFFER, rectCornersBuffer); | |
gl.vertexAttribPointer(shadingProgram.vertexPosition, 4, gl.FLOAT, false, 0, 0); | |
gl.enableVertexAttribArray(shadingProgram.vertexPosition); | |
gl.uniformMatrix4fv(shadingProgram.projectionMatrix, false, projectionMatrix); | |
gl.uniformMatrix4fv(shadingProgram.modelViewMatrix, false, modelViewMatrix); | |
gl.uniform1f(shadingProgram.far, 100.0); | |
gl.uniform1f(shadingProgram.time, time / 1000.0); | |
gl.uniform1i(shadingProgram.depthSoFar, 0); | |
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
// gl.useProgram(copyProgram); | |
// gl.bindBuffer(gl.ARRAY_BUFFER, rectCornersBuffer); | |
// gl.vertexAttribPointer(copyProgram.vertexPosition, 4, gl.FLOAT, false, 0, 0); | |
// gl.enableVertexAttribArray(copyProgram.vertexPosition); | |
// gl.bindTexture(gl.TEXTURE_2D, framebuffers[7].texture); | |
// gl.uniform1i(copyProgram.depthSoFar, 0); | |
// gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
// gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
stats.end(); | |
requestAnimationFrame(render); | |
} | |
function loadProgram(selector, attributes, uniforms) { | |
var programText = document.querySelector(selector).innerHTML; | |
var programParts = programText.split(/\n\s+\[(.+?)\]/); | |
const shaderProgram = gl.createProgram(); | |
for (let i = 1; i < programParts.length; i += 2) { | |
var headersParts = programParts[i].split(' '); | |
var name = headersParts[0]; | |
var source = programParts[0] + programParts[i+1]; | |
for (let j = 1; j < headersParts.length; j += 2) { | |
let m; | |
if (m = headersParts[j].match(/include:(.+)/)) { | |
source = document.querySelector(m[1]).innerHTML + source; | |
} else { | |
console.error('Unrecognized directive: ' + headersParts[j]); | |
} | |
} | |
var shader = gl.createShader(gl[name.toUpperCase() + '_SHADER']); | |
gl.shaderSource(shader, source); | |
gl.compileShader(shader); | |
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
console.error('An error occurred compiling the ' + name + ' shader: ' + gl.getShaderInfoLog(shader)); | |
let lineN = 0; | |
console.log(source.replace(/\n/g, (x) => `\n[${++lineN}]`)); | |
gl.deleteShader(shader); | |
} | |
gl.attachShader(shaderProgram, shader); | |
} | |
gl.linkProgram(shaderProgram); | |
for (let attribute of attributes) { | |
shaderProgram[attribute] = gl.getAttribLocation(shaderProgram, attribute) | |
} | |
for (let uniform of uniforms) { | |
shaderProgram[uniform] = gl.getUniformLocation(shaderProgram, uniform) | |
} | |
return shaderProgram; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment