Created
July 20, 2024 06:51
-
-
Save jeantimex/06221584bbe7f37c82e7c25ee55c993c to your computer and use it in GitHub Desktop.
This file contains 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> | |
<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