Skip to content

Instantly share code, notes, and snippets.

@jeantimex
Created July 19, 2024 18:09
Show Gist options
  • Save jeantimex/76ffdca437be2ac9b757940c6c3c903f to your computer and use it in GitHub Desktop.
Save jeantimex/76ffdca437be2ac9b757940c6c3c903f to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL2 Triangle, Plane, Point Light, and Rotating Shadow</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
</head>
<body>
<canvas id="glCanvas" width="800" height="600"></canvas>
<script>
const vsSource = `#version 300 es
in vec4 aPosition;
in vec4 aColor;
in vec3 aNormal;
out vec4 vColor;
out vec4 vPositionFromLight;
out vec3 vNormal;
out vec3 FragPos;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat4 uLightSpaceMatrix;
uniform mat4 uModelMatrix;
uniform mat4 uShadowModelMatrix;
void main() {
FragPos = vec3(uModelMatrix * aPosition);
gl_Position = uProjectionMatrix * uModelViewMatrix * aPosition;
vPositionFromLight = uLightSpaceMatrix * uShadowModelMatrix * aPosition;
vColor = aColor;
vNormal = mat3(transpose(inverse(uModelMatrix))) * aNormal;
}`;
const fsSource = `#version 300 es
precision highp float;
in vec4 vColor;
in vec4 vPositionFromLight;
in vec3 vNormal;
in vec3 FragPos;
out vec4 fragColor;
uniform sampler2D uShadowMap;
uniform vec3 lightPos;
float ShadowCalculation(vec4 fragPosLightSpace)
{
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
projCoords = projCoords * 0.5 + 0.5;
float closestDepth = texture(uShadowMap, projCoords.xy).r;
float currentDepth = projCoords.z;
vec3 normal = normalize(vNormal);
vec3 lightDir = normalize(lightPos - FragPos);
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
float shadow = 0.0;
vec2 texelSize = 1.0 / vec2(textureSize(uShadowMap, 0));
for(int x = -1; x <= 1; ++x)
{
for(int y = -1; y <= 1; ++y)
{
float pcfDepth = texture(uShadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
}
}
shadow /= 9.0;
if(projCoords.z > 1.0)
shadow = 0.0;
return shadow;
}
void main() {
float shadow = ShadowCalculation(vPositionFromLight);
vec3 lighting = (1.0 - shadow * 0.5) * vColor.rgb;
fragColor = vec4(lighting, 1.0);
}`;
const shadowVsSource = `#version 300 es
in vec4 aPosition;
uniform mat4 uLightSpaceMatrix;
uniform mat4 uShadowModelMatrix;
void main() {
gl_Position = uLightSpaceMatrix * uShadowModelMatrix * aPosition;
}`;
const shadowFsSource = `#version 300 es
precision highp float;
void main() {
// gl_FragDepth = gl_FragCoord.z;
}`;
let gl;
let programInfo;
let shadowProgramInfo;
let buffers;
let shadowFramebuffer;
let shadowMap;
function initGL() {
const canvas = document.getElementById('glCanvas');
gl = canvas.getContext('webgl2');
if (!gl) {
alert('Unable to initialize WebGL2. Your browser may not support it.');
return;
}
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
const shadowShaderProgram = initShaderProgram(gl, shadowVsSource, shadowFsSource);
programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aColor'),
vertexNormal: gl.getAttribLocation(shaderProgram, 'aNormal'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
lightSpaceMatrix: gl.getUniformLocation(shaderProgram, 'uLightSpaceMatrix'),
shadowMap: gl.getUniformLocation(shaderProgram, 'uShadowMap'),
modelMatrix: gl.getUniformLocation(shaderProgram, 'uModelMatrix'),
shadowModelMatrix: gl.getUniformLocation(shaderProgram, 'uShadowModelMatrix'),
lightPos: gl.getUniformLocation(shaderProgram, 'lightPos'),
},
};
shadowProgramInfo = {
program: shadowShaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shadowShaderProgram, 'aPosition'),
},
uniformLocations: {
lightSpaceMatrix: gl.getUniformLocation(shadowShaderProgram, 'uLightSpaceMatrix'),
shadowModelMatrix: gl.getUniformLocation(shadowShaderProgram, 'uShadowModelMatrix'),
},
};
buffers = initBuffers(gl);
initShadowMap();
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
render();
}
function initBuffers(gl) {
const positions = [
// Triangle vertices
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0,
// Plane vertices
-2.0, -1.5, -2.0,
2.0, -1.5, -2.0,
2.0, -1.5, 2.0,
-2.0, -1.5, 2.0,
];
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
const colors = [
// Triangle colors
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
// Plane colors
0.5, 0.5, 0.5, 1.0,
0.5, 0.5, 0.5, 1.0,
0.5, 0.5, 0.5, 1.0,
0.5, 0.5, 0.5, 1.0,
];
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
const normals = [
// Triangle normals
0.0, 0.0, 1.0,
0.0, 0.0, 1.0,
0.0, 0.0, 1.0,
// Plane normals
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
];
const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
const indices = [
// Triangle
0, 1, 2,
// Plane
3, 4, 5,
5, 6, 3
];
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
return {
position: positionBuffer,
color: colorBuffer,
normal: normalBuffer,
indices: indexBuffer,
};
}
function initShadowMap() {
const SHADOW_WIDTH = 4096,
SHADOW_HEIGHT = 4096;
shadowFramebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer);
shadowMap = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, shadowMap);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F,
SHADOW_WIDTH, SHADOW_HEIGHT, 0, gl.DEPTH_COMPONENT, gl.FLOAT, 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);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, shadowMap, 0);
gl.drawBuffers([gl.NONE]);
gl.readBuffer(gl.NONE);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
console.error('Framebuffer is not complete');
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
function render() {
const SHADOW_WIDTH = 4096,
SHADOW_HEIGHT = 4096;
// First render pass: render to shadow map
gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer);
gl.viewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
gl.clear(gl.DEPTH_BUFFER_BIT);
const lightProjectionMatrix = mat4.create();
const lightViewMatrix = mat4.create();
const lightSpaceMatrix = mat4.create();
// Set up light position and projection
const lightPos = [2, 4, -1];
mat4.ortho(lightProjectionMatrix, -10, 10, -10, 10, 1, 20);
mat4.lookAt(lightViewMatrix, lightPos, [0, 0, 0], [0, 1, 0]);
mat4.multiply(lightSpaceMatrix, lightProjectionMatrix, lightViewMatrix);
gl.useProgram(shadowProgramInfo.program);
gl.uniformMatrix4fv(shadowProgramInfo.uniformLocations.lightSpaceMatrix, false, lightSpaceMatrix);
// Draw scene for shadow map
drawScene(shadowProgramInfo, lightSpaceMatrix, true);
// Second render pass: render scene with shadows
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const fieldOfView = 45 * Math.PI / 180;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
const viewMatrix = mat4.create();
const eye = [0, 2, 5];
const center = [0, 0, 0];
const up = [0, 1, 0];
mat4.lookAt(viewMatrix, eye, center, up);
gl.useProgram(programInfo.program);
gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix);
gl.uniformMatrix4fv(programInfo.uniformLocations.lightSpaceMatrix, false, lightSpaceMatrix);
gl.uniform3fv(programInfo.uniformLocations.lightPos, lightPos);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, shadowMap);
gl.uniform1i(programInfo.uniformLocations.shadowMap, 0);
// Draw scene with shadows
drawScene(programInfo, viewMatrix, false);
requestAnimationFrame(render);
}
function drawScene(program, viewMatrix, isShadowPass) {
const rotation = performance.now() * 0.001;
// Triangle model matrix
const triangleModelMatrix = mat4.create();
mat4.translate(triangleModelMatrix, triangleModelMatrix, [0.0, 0.5, -2.0]);
mat4.rotate(triangleModelMatrix, triangleModelMatrix, rotation, [0, 1, 0]);
// Shadow model matrix (without vertical offset)
const triangleShadowModelMatrix = mat4.create();
mat4.translate(triangleShadowModelMatrix, triangleShadowModelMatrix, [0.0, -1.0, -2.0]);
mat4.rotate(triangleShadowModelMatrix, triangleShadowModelMatrix, rotation, [0, 1, 0]);
// Plane model matrix
const planeModelMatrix = mat4.create();
mat4.translate(planeModelMatrix, planeModelMatrix, [0.0, 0.0, -2.0]);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
gl.vertexAttribPointer(program.attribLocations.vertexPosition, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.attribLocations.vertexPosition);
if (program.attribLocations.vertexColor !== undefined) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);
gl.vertexAttribPointer(program.attribLocations.vertexColor, 4, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.attribLocations.vertexColor);
}
if (program.attribLocations.vertexNormal !== undefined) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal);
gl.vertexAttribPointer(program.attribLocations.vertexNormal, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.attribLocations.vertexNormal);
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);
// Draw triangle
let modelViewMatrix = mat4.create();
mat4.multiply(modelViewMatrix, viewMatrix, triangleModelMatrix);
gl.uniformMatrix4fv(program.uniformLocations.modelViewMatrix, false, modelViewMatrix);
gl.uniformMatrix4fv(program.uniformLocations.modelMatrix, false, triangleModelMatrix);
gl.uniformMatrix4fv(program.uniformLocations.shadowModelMatrix, false, triangleShadowModelMatrix);
gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);
// Draw plane
modelViewMatrix = mat4.create();
mat4.multiply(modelViewMatrix, viewMatrix, planeModelMatrix);
gl.uniformMatrix4fv(program.uniformLocations.modelViewMatrix, false, modelViewMatrix);
gl.uniformMatrix4fv(program.uniformLocations.modelMatrix, false, planeModelMatrix);
gl.uniformMatrix4fv(program.uniformLocations.shadowModelMatrix, false, planeModelMatrix);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 6);
}
function initShaderProgram(gl, vsSource, fsSource) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
return null;
}
return shaderProgram;
}
function loadShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
window.onload = initGL;
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment