Skip to content

Instantly share code, notes, and snippets.

@jeantimex
Created July 20, 2024 06:51
Show Gist options
  • Save jeantimex/06221584bbe7f37c82e7c25ee55c993c to your computer and use it in GitHub Desktop.
Save jeantimex/06221584bbe7f37c82e7c25ee55c993c 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 Red Cube with Lighting and 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>
// Vertex Shader for main rendering
const vsSource = `#version 300 es
// Input attributes
in vec4 aPosition; // Vertex position in model space
in vec3 aNormal; // Vertex normal in model space
// Output variables to fragment shader
out vec3 vNormal; // Transformed normal in world space
out vec3 vFragPos; // Fragment position in world space
out vec4 vPositionFromLight; // Fragment position from light's perspective
// Uniform matrices
uniform mat4 uModelViewMatrix; // Combined model and view matrix
uniform mat4 uProjectionMatrix; // Projection matrix
uniform mat4 uLightSpaceMatrix; // Light space transformation matrix
uniform mat4 uModelMatrix; // Model matrix
uniform mat4 uShadowModelMatrix; // Model matrix for shadow calculations
void main() {
// Calculate fragment position in world space
vFragPos = vec3(uModelMatrix * aPosition);
// Calculate final position on screen
gl_Position = uProjectionMatrix * uModelViewMatrix * aPosition;
// Transform vertex position to light space for shadow mapping
vPositionFromLight = uLightSpaceMatrix * uShadowModelMatrix * aPosition;
// Transform normal to world space
vNormal = mat3(transpose(inverse(uModelMatrix))) * aNormal;
}
`;
// Fragment Shader for main rendering
const fsSource = `#version 300 es
precision highp float;
// Input variables from vertex shader
in vec3 vNormal;
in vec3 vFragPos;
in vec4 vPositionFromLight;
// Output color
out vec4 fragColor;
// Uniform variables
uniform vec3 uLightPos; // Light position in world space
uniform sampler2D uShadowMap; // Shadow map texture
// Function to calculate shadow factor
float ShadowCalculation(vec4 fragPosLightSpace) {
// Perform perspective divide and map to [0,1] range
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
projCoords = projCoords * 0.5 + 0.5;
// Get depth of current fragment from light's perspective
float currentDepth = projCoords.z;
// Calculate bias to reduce shadow acne
float bias = max(0.05 * (1.0 - dot(vNormal, normalize(uLightPos - vFragPos))), 0.005);
// Percentage-closer filtering (PCF)
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;
// Avoid shadow outside of far plane
if(projCoords.z > 1.0)
shadow = 0.0;
return shadow;
}
void main() {
vec3 lightColor = vec3(1.0, 1.0, 1.0);
vec3 objectColor = vec3(1.0, 0.0, 0.0); // Red color for the cube
// Ambient lighting
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// Diffuse lighting
vec3 norm = normalize(vNormal);
vec3 lightDir = normalize(uLightPos - vFragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// Specular lighting
float specularStrength = 0.5;
vec3 viewDir = normalize(-vFragPos); // Assuming camera at (0,0,0)
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
vec3 specular = specularStrength * spec * lightColor;
// Combine lighting components
vec3 lighting = (ambient + diffuse + specular) * objectColor;
// Apply shadow only to the plane
if (vFragPos.y < -1.4) { // Assuming the plane is at y = -1.5
float shadow = ShadowCalculation(vPositionFromLight);
lighting *= (1.0 - shadow * 0.5);
}
fragColor = vec4(lighting, 1.0);
}
`;
// Vertex Shader for shadow mapping
const shadowVsSource = `#version 300 es
in vec4 aPosition; // Vertex position
uniform mat4 uLightSpaceMatrix; // Light space transformation matrix
uniform mat4 uShadowModelMatrix; // Model matrix for shadow calculations
void main() {
// Transform vertex to light space for shadow mapping
gl_Position = uLightSpaceMatrix * uShadowModelMatrix * aPosition;
}
`;
// Fragment Shader for shadow mapping
const shadowFsSource = `#version 300 es
precision highp float;
void main() {
// For shadow mapping, we only need the depth information
// which is automatically written to the depth buffer
// gl_FragDepth = gl_FragCoord.z; // Uncomment if custom depth is needed
}
`;
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'),
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, 'uLightPos'),
},
};
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) {
// Define the vertices for a cube and a plane
const positions = [
// Cube vertices (24 vertices: 4 for each of 6 faces)
// Front face
-0.5, -0.5, 0.5, // Bottom-left
0.5, -0.5, 0.5, // Bottom-right
0.5, 0.5, 0.5, // Top-right
-0.5, 0.5, 0.5, // Top-left
// Back face
-0.5, -0.5, -0.5, // Bottom-left
-0.5, 0.5, -0.5, // Top-left
0.5, 0.5, -0.5, // Top-right
0.5, -0.5, -0.5, // Bottom-right
// Top face
-0.5, 0.5, -0.5, // Back-left
-0.5, 0.5, 0.5, // Front-left
0.5, 0.5, 0.5, // Front-right
0.5, 0.5, -0.5, // Back-right
// Bottom face
-0.5, -0.5, -0.5, // Back-left
0.5, -0.5, -0.5, // Back-right
0.5, -0.5, 0.5, // Front-right
-0.5, -0.5, 0.5, // Front-left
// Right face
0.5, -0.5, -0.5, // Bottom-back
0.5, 0.5, -0.5, // Top-back
0.5, 0.5, 0.5, // Top-front
0.5, -0.5, 0.5, // Bottom-front
// Left face
-0.5, -0.5, -0.5, // Bottom-back
-0.5, -0.5, 0.5, // Bottom-front
-0.5, 0.5, 0.5, // Top-front
-0.5, 0.5, -0.5, // Top-back
// Plane vertices (4 vertices for a square plane)
-3.0, -1.5, -3.0, // Back-left
3.0, -1.5, -3.0, // Back-right
3.0, -1.5, 3.0, // Front-right
-3.0, -1.5, 3.0, // Front-left
];
// Create and bind the position buffer
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// Define normal vectors for lighting calculations
const normals = [
// Cube normals (24 normals: 1 for each vertex)
// Front face: all normals point towards positive Z
0.0, 0.0, 1.0,
0.0, 0.0, 1.0,
0.0, 0.0, 1.0,
0.0, 0.0, 1.0,
// Back face: all normals point towards negative Z
0.0, 0.0, -1.0,
0.0, 0.0, -1.0,
0.0, 0.0, -1.0,
0.0, 0.0, -1.0,
// Top face: all normals point towards positive Y
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
// Bottom face: all normals point towards negative Y
0.0, -1.0, 0.0,
0.0, -1.0, 0.0,
0.0, -1.0, 0.0,
0.0, -1.0, 0.0,
// Right face: all normals point towards positive X
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
// Left face: all normals point towards negative X
-1.0, 0.0, 0.0,
-1.0, 0.0, 0.0,
-1.0, 0.0, 0.0,
-1.0, 0.0, 0.0,
// Plane normals: all point upwards (positive Y)
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
];
// Create and bind the normal buffer
const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
// Define indices for drawing the triangles
const indices = [
// Cube indices (36 indices: 6 faces * 2 triangles per face * 3 vertices per triangle)
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // back
8, 9, 10, 8, 10, 11, // top
12, 13, 14, 12, 14, 15, // bottom
16, 17, 18, 16, 18, 19, // right
20, 21, 22, 20, 22, 23, // left
// Plane indices (6 indices: 2 triangles * 3 vertices per triangle)
24, 25, 26, 26, 27, 24
];
// Create and bind the index buffer
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
// Return the created buffers
return {
position: positionBuffer,
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() {
// Set the dimensions for the shadow map
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);
// Create matrices for light's perspective
const lightProjectionMatrix = mat4.create();
const lightViewMatrix = mat4.create();
const lightSpaceMatrix = mat4.create();
// Set up light position and target
const lightPos = [15, 20, 0];
const lookAtPoint = [0, 0, 0];
// Calculate and normalize light direction
const lightDir = vec3.create();
vec3.subtract(lightDir, lookAtPoint, lightPos);
vec3.normalize(lightDir, lightDir);
// Choose an initial up vector for the light
let worldUp = [0, 1, 0];
// If light direction is too close to world up, use a different axis
if (Math.abs(vec3.dot(lightDir, worldUp)) > 0.99) {
worldUp = [1, 0, 0];
}
// Calculate right vector for the light's coordinate system
const rightVector = vec3.create();
vec3.cross(rightVector, worldUp, lightDir);
vec3.normalize(rightVector, rightVector);
// Calculate up vector for the light
const upVector = vec3.create();
vec3.cross(upVector, lightDir, rightVector);
// Create orthographic projection for the light
mat4.ortho(lightProjectionMatrix, -10, 10, -10, 10, 0.1, 100.0);
// Create view matrix for the light
mat4.lookAt(lightViewMatrix, lightPos, lookAtPoint, upVector);
// Combine projection and view matrices for light space transformation
mat4.multiply(lightSpaceMatrix, lightProjectionMatrix, lightViewMatrix);
// Use shadow shader program
gl.useProgram(shadowProgramInfo.program);
gl.uniformMatrix4fv(shadowProgramInfo.uniformLocations.lightSpaceMatrix, false, lightSpaceMatrix);
// Render the scene from light's perspective (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);
// Set up perspective projection for the camera
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);
// Set up camera view
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);
// Use main shader program
gl.useProgram(programInfo.program);
// Set uniforms for main shader
gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix);
gl.uniformMatrix4fv(programInfo.uniformLocations.lightSpaceMatrix, false, lightSpaceMatrix);
gl.uniform3fv(programInfo.uniformLocations.lightPos, lightPos);
// Bind shadow map texture
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, shadowMap);
gl.uniform1i(programInfo.uniformLocations.shadowMap, 0);
// Render the scene with shadows
drawScene(programInfo, viewMatrix, false);
// Request next frame
requestAnimationFrame(render);
}
function drawScene(program, viewMatrix, isShadowPass) {
const rotation = performance.now() * 0.001;
// Cube model matrix
const cubeModelMatrix = mat4.create();
mat4.translate(cubeModelMatrix, cubeModelMatrix, [0.0, 0.5, -2.0]);
mat4.rotate(cubeModelMatrix, cubeModelMatrix, rotation, [0, 1, 0]);
mat4.rotate(cubeModelMatrix, cubeModelMatrix, rotation * 0.7, [1, 0, 0]);
// Shadow model matrix (without vertical offset)
const cubeShadowModelMatrix = mat4.create();
mat4.translate(cubeShadowModelMatrix, cubeShadowModelMatrix, [0.0, -1.0, -2.0]);
mat4.rotate(cubeShadowModelMatrix, cubeShadowModelMatrix, rotation, [0, 1, 0]);
mat4.rotate(cubeShadowModelMatrix, cubeShadowModelMatrix, rotation * 0.7, [1, 0, 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.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 cube
let modelViewMatrix = mat4.create();
mat4.multiply(modelViewMatrix, viewMatrix, cubeModelMatrix);
gl.uniformMatrix4fv(program.uniformLocations.modelViewMatrix, false, modelViewMatrix);
gl.uniformMatrix4fv(program.uniformLocations.modelMatrix, false, cubeModelMatrix);
gl.uniformMatrix4fv(program.uniformLocations.shadowModelMatrix, false, cubeShadowModelMatrix);
gl.drawElements(gl.TRIANGLES, 36, 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, 72);
}
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