Created
March 22, 2024 17:34
-
-
Save greggman/d953b936a9db511784c094c16d02c544 to your computer and use it in GitHub Desktop.
WebGL2 - Shadows - Basic w/spot light (PCF)
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
@import url("https://webgl2fundamentals.org/webgl/resources/webgl-tutorials.css"); | |
body { | |
margin: 0; | |
} | |
canvas { | |
width: 100vw; | |
height: 100vh; | |
display: block; | |
} | |
.gman-widget-value { | |
min-width: 5em; | |
} |
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
<canvas id="canvas"></canvas> | |
<div id="uiContainer"> | |
<div id="ui"> | |
</div> | |
</div> | |
<!-- | |
This sample uses TWGL (Tiny WebGL) to hide the clutter. | |
Otherwise the sample would be full of code not related to the point of the sample. | |
For more info see https://webgl2fundamentals.org/webgl/lessons/webgl-less-code-more-fun.html | |
--> | |
<script src="https://webgl2fundamentals.org/webgl/resources/webgl-lessons-ui.js"></script> | |
<script src="https://webgl2fundamentals.org/webgl/resources/twgl-full.min.js"></script> | |
<script src="https://webgl2fundamentals.org/webgl/resources/m4.js"></script> |
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
// WebGL2 - Shadows - Basic w/spot light | |
// from https://webgl2fundamentals.org/webgl/webgl-shadows-w-spot-light.html | |
'use strict'; | |
const vs = `#version 300 es | |
in vec4 a_position; | |
in vec2 a_texcoord; | |
in vec3 a_normal; | |
uniform vec3 u_lightWorldPosition; | |
uniform vec3 u_viewWorldPosition; | |
uniform mat4 u_projection; | |
uniform mat4 u_view; | |
uniform mat4 u_world; | |
uniform mat4 u_textureMatrix; | |
out vec2 v_texcoord; | |
out vec4 v_projectedTexcoord; | |
out vec3 v_normal; | |
out vec3 v_surfaceToLight; | |
out vec3 v_surfaceToView; | |
void main() { | |
// Multiply the position by the matrix. | |
vec4 worldPosition = u_world * a_position; | |
gl_Position = u_projection * u_view * worldPosition; | |
// Pass the texture coord to the fragment shader. | |
v_texcoord = a_texcoord; | |
v_projectedTexcoord = u_textureMatrix * worldPosition; | |
// orient the normals and pass to the fragment shader | |
v_normal = mat3(u_world) * a_normal; | |
// compute the world position of the surface | |
vec3 surfaceWorldPosition = (u_world * a_position).xyz; | |
// compute the vector of the surface to the light | |
// and pass it to the fragment shader | |
v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition; | |
// compute the vector of the surface to the view/camera | |
// and pass it to the fragment shader | |
v_surfaceToView = u_viewWorldPosition - surfaceWorldPosition; | |
} | |
`; | |
const fs = `#version 300 es | |
precision highp float; | |
in vec2 v_texcoord; | |
in vec4 v_projectedTexcoord; | |
in vec3 v_normal; | |
in vec3 v_surfaceToLight; | |
in vec3 v_surfaceToView; | |
uniform vec4 u_colorMult; | |
uniform sampler2D u_texture; | |
uniform sampler2D u_projectedTexture; | |
uniform float u_bias; | |
uniform float u_shininess; | |
uniform vec3 u_lightDirection; | |
uniform float u_innerLimit; | |
uniform float u_outerLimit; | |
// no need to uniform at this sample | |
float u_shadowVisibility=0.5; | |
// to uniform --> | |
//uniform float u_shadowVisibility;// shadow visibility | |
// uniform location at init time | |
//var blabla=gl.getUniformLocation(program, "u_shadowVisibility"); | |
// uniform at draw time | |
//gl.uniform1f(blabla, your value);//0.0-1.0 uniform <-- | |
out vec4 outColor; | |
void main() | |
{ | |
vec3 normal=normalize(v_normal); | |
vec3 surfaceToLightDirection=normalize(v_surfaceToLight); | |
vec3 surfaceToViewDirection=normalize(v_surfaceToView); | |
vec3 halfVector=normalize(surfaceToLightDirection + surfaceToViewDirection); | |
float dotFromDirection=dot(surfaceToLightDirection,-u_lightDirection); | |
float limitRange=u_innerLimit - u_outerLimit; | |
float inLight=clamp((dotFromDirection - u_outerLimit) / limitRange, 0.0, 1.0); | |
float light=inLight * dot(normal, surfaceToLightDirection); | |
float specular=inLight * pow(dot(normal, halfVector), u_shininess); | |
// shadow --> | |
vec3 projectedTexcoord=v_projectedTexcoord.xyz/v_projectedTexcoord.w; | |
float currentDepth=projectedTexcoord.z+u_bias; | |
float projectedDepth=texture(u_projectedTexture, projectedTexcoord.xy).r; | |
bool inRange=projectedTexcoord.x>=0.0&&projectedTexcoord.x<=1.0&&projectedTexcoord.y>=0.0&&projectedTexcoord.y<=1.0; | |
//float shadowLight=(inRange&&projectedDepth<=currentDepth)?0.0:1.0; | |
float shadowLight=0.0; | |
// anti aliasing/pcf (percentage-closer filtering) | |
vec2 texSize=vec2(1)/vec2(textureSize(u_projectedTexture, 0)); | |
int samples=6; | |
for(int x=0;x<samples;++x){ for(int y=0;y<samples;++y) | |
{ | |
float pcf=texture(u_projectedTexture, projectedTexcoord.xy+vec2(x, y)*texSize).r; | |
shadowLight+=inRange&¤tDepth>pcf?u_shadowVisibility:1.0; | |
}} | |
// final scene brightnes. needs to be adjusted by the | |
shadowLight/=9.0*4.0;// amount of samples | |
// keep the shadow at 0.0 if outside of region | |
if(projectedTexcoord.z>1.0)shadowLight=0.0; | |
// shadow <-- | |
vec4 texColor=texture(u_texture, v_texcoord) * u_colorMult; | |
outColor=vec4(texColor.rgb * light * shadowLight +specular * shadowLight,texColor.a); | |
}`; | |
const colorVS = `#version 300 es | |
in vec4 a_position; | |
uniform mat4 u_projection; | |
uniform mat4 u_view; | |
uniform mat4 u_world; | |
void main() { | |
// Multiply the position by the matrices. | |
gl_Position = u_projection * u_view * u_world * a_position; | |
} | |
`; | |
const colorFS = `#version 300 es | |
precision highp float; | |
uniform vec4 u_color; | |
out vec4 outColor; | |
void main() { | |
outColor = u_color; | |
} | |
`; | |
function main() { | |
// Get A WebGL context | |
/** @type {HTMLCanvasElement} */ | |
const canvas = document.querySelector('#canvas'); | |
const gl = canvas.getContext('webgl2'); | |
if (!gl) { | |
return; | |
} | |
// setup GLSL programs | |
// note: Since we're going to use the same VAO with multiple | |
// shader programs we need to make sure all programs use the | |
// same attribute locations. There are 2 ways to do that. | |
// (1) assign them in GLSL. (2) assign them by calling `gl.bindAttribLocation` | |
// before linking. We're using method 2 as it's more. D.R.Y. | |
const programOptions = { | |
attribLocations: { | |
'a_position': 0, | |
'a_normal': 1, | |
'a_texcoord': 2, | |
'a_color': 3, | |
}, | |
}; | |
const textureProgramInfo = twgl.createProgramInfo(gl, [vs, fs], programOptions); | |
const colorProgramInfo = twgl.createProgramInfo(gl, [colorVS, colorFS], programOptions); | |
// Tell the twgl to match position with a_position, | |
// normal with a_normal etc.. | |
twgl.setAttributePrefix("a_"); | |
const sphereBufferInfo = twgl.primitives.createSphereBufferInfo( | |
gl, | |
1, // radius | |
32, // subdivisions around | |
24, // subdivisions down | |
); | |
const sphereVAO = twgl.createVAOFromBufferInfo( | |
gl, textureProgramInfo, sphereBufferInfo); | |
const planeBufferInfo = twgl.primitives.createPlaneBufferInfo( | |
gl, | |
20, // width | |
20, // height | |
1, // subdivisions across | |
1, // subdivisions down | |
); | |
const planeVAO = twgl.createVAOFromBufferInfo( | |
gl, textureProgramInfo, planeBufferInfo); | |
const cubeBufferInfo = twgl.primitives.createCubeBufferInfo( | |
gl, | |
2, // size | |
); | |
const cubeVAO = twgl.createVAOFromBufferInfo( | |
gl, textureProgramInfo, cubeBufferInfo); | |
const cubeLinesBufferInfo = twgl.createBufferInfoFromArrays(gl, { | |
position: [ | |
-1, -1, -1, | |
1, -1, -1, | |
-1, 1, -1, | |
1, 1, -1, | |
-1, -1, 1, | |
1, -1, 1, | |
-1, 1, 1, | |
1, 1, 1, | |
], | |
indices: [ | |
0, 1, | |
1, 3, | |
3, 2, | |
2, 0, | |
4, 5, | |
5, 7, | |
7, 6, | |
6, 4, | |
0, 4, | |
1, 5, | |
3, 7, | |
2, 6, | |
], | |
}); | |
const cubeLinesVAO = twgl.createVAOFromBufferInfo( | |
gl, colorProgramInfo, cubeLinesBufferInfo); | |
// make a 8x8 checkerboard texture | |
const checkerboardTexture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, checkerboardTexture); | |
gl.texImage2D( | |
gl.TEXTURE_2D, | |
0, // mip level | |
gl.LUMINANCE, // internal format | |
8, // width | |
8, // height | |
0, // border | |
gl.LUMINANCE, // format | |
gl.UNSIGNED_BYTE, // type | |
new Uint8Array([ // data | |
0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, | |
0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, | |
0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, | |
0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, | |
0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, | |
0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, | |
0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, | |
0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, | |
])); | |
gl.generateMipmap(gl.TEXTURE_2D); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
const depthTexture = gl.createTexture(); | |
const depthTextureSize = 512; | |
gl.bindTexture(gl.TEXTURE_2D, depthTexture); | |
gl.texImage2D( | |
gl.TEXTURE_2D, // target | |
0, // mip level | |
gl.DEPTH_COMPONENT32F, // internal format | |
depthTextureSize, // width | |
depthTextureSize, // height | |
0, // border | |
gl.DEPTH_COMPONENT, // format | |
gl.FLOAT, // type | |
null); // data | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_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); | |
const depthFramebuffer = gl.createFramebuffer(); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer); | |
gl.framebufferTexture2D( | |
gl.FRAMEBUFFER, // target | |
gl.DEPTH_ATTACHMENT, // attachment point | |
gl.TEXTURE_2D, // texture target | |
depthTexture, // texture | |
0); // mip level | |
function degToRad(d) { | |
return d * Math.PI / 180; | |
} | |
const settings = { | |
cameraX: 6, | |
cameraY: 5, | |
posX: 2.5, | |
posY: 4.8, | |
posZ: 4.3, | |
targetX: 2.5, | |
targetY: 0, | |
targetZ: 3.5, | |
projWidth: 1, | |
projHeight: 1, | |
perspective: true, | |
fieldOfView: 120, | |
bias: -0.006, | |
}; | |
webglLessonsUI.setupUI(document.querySelector('#ui'), settings, [ | |
{ type: 'slider', key: 'cameraX', min: -10, max: 10, change: render, precision: 2, step: 0.001, }, | |
{ type: 'slider', key: 'cameraY', min: 1, max: 20, change: render, precision: 2, step: 0.001, }, | |
{ type: 'slider', key: 'posX', min: -10, max: 10, change: render, precision: 2, step: 0.001, }, | |
{ type: 'slider', key: 'posY', min: 1, max: 20, change: render, precision: 2, step: 0.001, }, | |
{ type: 'slider', key: 'posZ', min: 1, max: 20, change: render, precision: 2, step: 0.001, }, | |
{ type: 'slider', key: 'targetX', min: -10, max: 10, change: render, precision: 2, step: 0.001, }, | |
{ type: 'slider', key: 'targetY', min: 0, max: 20, change: render, precision: 2, step: 0.001, }, | |
{ type: 'slider', key: 'targetZ', min: -10, max: 20, change: render, precision: 2, step: 0.001, }, | |
{ type: 'slider', key: 'projWidth', min: 0, max: 2, change: render, precision: 2, step: 0.001, }, | |
{ type: 'slider', key: 'projHeight', min: 0, max: 2, change: render, precision: 2, step: 0.001, }, | |
{ type: 'checkbox', key: 'perspective', change: render, }, | |
{ type: 'slider', key: 'fieldOfView', min: 1, max: 179, change: render, }, | |
{ type: 'slider', key: 'bias', min: -0.01, max: 0.00001, change: render, precision: 4, step: 0.0001, }, | |
]); | |
const fieldOfViewRadians = degToRad(60); | |
// Uniforms for each object. | |
const planeUniforms = { | |
u_colorMult: [0.5, 0.5, 1, 1], // lightblue | |
u_color: [1, 0, 0, 1], | |
u_texture: checkerboardTexture, | |
u_world: m4.translation(0, 0, 0), | |
}; | |
const sphereUniforms = { | |
u_colorMult: [1, 0.5, 0.5, 1], // pink | |
u_color: [0, 0, 1, 1], | |
u_texture: checkerboardTexture, | |
u_world: m4.translation(2, 3, 4), | |
}; | |
const cubeUniforms = { | |
u_colorMult: [0.5, 1, 0.5, 1], // lightgreen | |
u_color: [0, 0, 1, 1], | |
u_texture: checkerboardTexture, | |
u_world: m4.translation(3, 1, 0), | |
}; | |
function drawScene( | |
projectionMatrix, | |
cameraMatrix, | |
textureMatrix, | |
lightWorldMatrix, | |
programInfo) { | |
// Make a view matrix from the camera matrix. | |
const viewMatrix = m4.inverse(cameraMatrix); | |
gl.useProgram(programInfo.program); | |
// set uniforms that are the same for both the sphere and plane | |
// note: any values with no corresponding uniform in the shader | |
// are ignored. | |
twgl.setUniforms(programInfo, { | |
u_view: viewMatrix, | |
u_projection: projectionMatrix, | |
u_bias: settings.bias, | |
u_textureMatrix: textureMatrix, | |
u_projectedTexture: depthTexture, | |
u_shininess: 150, | |
u_innerLimit: Math.cos(degToRad(settings.fieldOfView / 2 - 10)), | |
u_outerLimit: Math.cos(degToRad(settings.fieldOfView / 2)), | |
u_lightDirection: lightWorldMatrix.slice(8, 11).map(v => -v), | |
u_lightWorldPosition: [settings.posX, settings.posY, settings.posZ], | |
u_viewWorldPosition: cameraMatrix.slice(12, 15), | |
}); | |
// ------ Draw the sphere -------- | |
// Setup all the needed attributes. | |
gl.bindVertexArray(sphereVAO); | |
// Set the uniforms unique to the sphere | |
twgl.setUniforms(programInfo, sphereUniforms); | |
// calls gl.drawArrays or gl.drawElements | |
twgl.drawBufferInfo(gl, sphereBufferInfo); | |
// ------ Draw the cube -------- | |
// Setup all the needed attributes. | |
gl.bindVertexArray(cubeVAO); | |
// Set the uniforms unique to the cube | |
twgl.setUniforms(programInfo, cubeUniforms); | |
// calls gl.drawArrays or gl.drawElements | |
twgl.drawBufferInfo(gl, cubeBufferInfo); | |
// ------ Draw the plane -------- | |
// Setup all the needed attributes. | |
gl.bindVertexArray(planeVAO); | |
// Set the uniforms unique to the cube | |
twgl.setUniforms(programInfo, planeUniforms); | |
// calls gl.drawArrays or gl.drawElements | |
twgl.drawBufferInfo(gl, planeBufferInfo); | |
} | |
// Draw the scene. | |
function render() { | |
twgl.resizeCanvasToDisplaySize(gl.canvas); | |
gl.enable(gl.CULL_FACE); | |
gl.enable(gl.DEPTH_TEST); | |
// first draw from the POV of the light | |
const lightWorldMatrix = m4.lookAt( | |
[settings.posX, settings.posY, settings.posZ], // position | |
[settings.targetX, settings.targetY, settings.targetZ], // target | |
[0, 1, 0], // up | |
); | |
const lightProjectionMatrix = settings.perspective | |
? m4.perspective( | |
degToRad(settings.fieldOfView), | |
settings.projWidth / settings.projHeight, | |
0.5, // near | |
10) // far | |
: m4.orthographic( | |
-settings.projWidth / 2, // left | |
settings.projWidth / 2, // right | |
-settings.projHeight / 2, // bottom | |
settings.projHeight / 2, // top | |
0.5, // near | |
10); // far | |
// draw to the depth texture | |
gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer); | |
gl.viewport(0, 0, depthTextureSize, depthTextureSize); | |
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); | |
drawScene( | |
lightProjectionMatrix, | |
lightWorldMatrix, | |
m4.identity(), | |
lightWorldMatrix, | |
colorProgramInfo); | |
// now draw scene to the canvas projecting the depth texture into the scene | |
gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); | |
gl.clearColor(0, 0, 0, 1); | |
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); | |
let textureMatrix = m4.identity(); | |
textureMatrix = m4.translate(textureMatrix, 0.5, 0.5, 0.5); | |
textureMatrix = m4.scale(textureMatrix, 0.5, 0.5, 0.5); | |
textureMatrix = m4.multiply(textureMatrix, lightProjectionMatrix); | |
// use the inverse of this world matrix to make | |
// a matrix that will transform other positions | |
// to be relative this this world space. | |
textureMatrix = m4.multiply( | |
textureMatrix, | |
m4.inverse(lightWorldMatrix)); | |
// Compute the projection matrix | |
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; | |
const projectionMatrix = | |
m4.perspective(fieldOfViewRadians, aspect, 1, 2000); | |
// Compute the camera's matrix using look at. | |
const cameraPosition = [settings.cameraX, settings.cameraY, 7]; | |
const target = [0, 0, 0]; | |
const up = [0, 1, 0]; | |
const cameraMatrix = m4.lookAt(cameraPosition, target, up); | |
drawScene( | |
projectionMatrix, | |
cameraMatrix, | |
textureMatrix, | |
lightWorldMatrix, | |
textureProgramInfo); | |
// ------ Draw the frustum ------ | |
{ | |
const viewMatrix = m4.inverse(cameraMatrix); | |
gl.useProgram(colorProgramInfo.program); | |
// Setup all the needed attributes. | |
gl.bindVertexArray(cubeLinesVAO); | |
// scale the cube in Z so it's really long | |
// to represent the texture is being projected to | |
// infinity | |
const mat = m4.multiply( | |
lightWorldMatrix, m4.inverse(lightProjectionMatrix)); | |
// Set the uniforms we just computed | |
twgl.setUniforms(colorProgramInfo, { | |
u_color: [1, 1, 1, 1], | |
u_view: viewMatrix, | |
u_projection: projectionMatrix, | |
u_world: mat, | |
}); | |
// calls gl.drawArrays or gl.drawElements | |
twgl.drawBufferInfo(gl, cubeLinesBufferInfo, gl.LINES); | |
} | |
} | |
render(); | |
} | |
main(); |
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
{"name":"WebGL2 - Shadows - Basic w/spot light (PCF)","settings":{},"filenames":["index.html","index.css","index.js"]} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment