Skip to content

Instantly share code, notes, and snippets.

@bojidar-bg
Created July 30, 2018 19:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bojidar-bg/12e7ea568750797cf504538cad4f0e4a to your computer and use it in GitHub Desktop.
Save bojidar-bg/12e7ea568750797cf504538cad4f0e4a to your computer and use it in GitHub Desktop.
WebGL Raymarcher
<!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