diff --git a/examples/files.js b/examples/files.js index 7e42574dd0ad26288036fea472b8c8f60290007e..a224328d5df959a2cc780fadc8ff4202d3b44928 100644 --- a/examples/files.js +++ b/examples/files.js @@ -288,6 +288,7 @@ var files = { "webgl_buffergeometry_rawshader", "webgl_buffergeometry_selective_draw", "webgl_buffergeometry_uint", + "webgl_cascadedshadowmaps", "webgl_custom_attributes", "webgl_custom_attributes_lines", "webgl_custom_attributes_points", diff --git a/examples/js/libs/three-csm.module.js b/examples/js/libs/three-csm.module.js new file mode 100644 index 0000000000000000000000000000000000000000..214ffae36ba871666fdb540a13cc5f9213e9b9df --- /dev/null +++ b/examples/js/libs/three-csm.module.js @@ -0,0 +1,531 @@ +import { Vector3, ShaderChunk, DirectionalLight, Vector2, LineBasicMaterial, Object3D, Geometry, Line } from '../../../build/three.module.js'; + +class FrustumVertex { + constructor(x, y, z) { + this.x = x || 0; + this.y = y || 0; + this.z = z || 0; + } + + fromLerp(v1, v2, amount) { + this.x = (1 - amount) * v1.x + amount * v2.x; + this.y = (1 - amount) * v1.y + amount * v2.y; + this.z = (1 - amount) * v1.z + amount * v2.z; + + return this; + } +} + +function toRad(degrees) { + return degrees * Math.PI / 180; +} + +class Frustum { + constructor(data) { + data = data || {}; + + this.fov = data.fov || 70; + this.near = data.near || 0.1; + this.far = data.far || 1000; + this.aspect = data.aspect || 1; + + this.vertices = { + near: [], + far: [] + }; + } + + getViewSpaceVertices() { + this.nearPlaneY = this.near * Math.tan(toRad(this.fov / 2)); + this.nearPlaneX = this.aspect * this.nearPlaneY; + + this.farPlaneY = this.far * Math.tan(toRad(this.fov / 2)); + this.farPlaneX = this.aspect * this.farPlaneY; + + // 3 --- 0 vertices.near/far order + // | | + // 2 --- 1 + + this.vertices.near.push( + new FrustumVertex(this.nearPlaneX, this.nearPlaneY, -this.near), + new FrustumVertex(this.nearPlaneX, -this.nearPlaneY, -this.near), + new FrustumVertex(-this.nearPlaneX, -this.nearPlaneY, -this.near), + new FrustumVertex(-this.nearPlaneX, this.nearPlaneY, -this.near) + ); + + this.vertices.far.push( + new FrustumVertex(this.farPlaneX, this.farPlaneY, -this.far), + new FrustumVertex(this.farPlaneX, -this.farPlaneY, -this.far), + new FrustumVertex(-this.farPlaneX, -this.farPlaneY, -this.far), + new FrustumVertex(-this.farPlaneX, this.farPlaneY, -this.far) + ); + + return this.vertices; + } + + split(breaks) { + const result = []; + + for(let i = 0; i < breaks.length; i++) { + const cascade = new Frustum(); + + if(i === 0) { + cascade.vertices.near = this.vertices.near; + } else { + for(let j = 0; j < 4; j++) { + cascade.vertices.near.push(new FrustumVertex().fromLerp(this.vertices.near[j], this.vertices.far[j], breaks[i - 1])); + } + } + + if(i === breaks - 1) { + cascade.vertices.far = this.vertices.far; + } else { + for(let j = 0; j < 4; j++) { + cascade.vertices.far.push(new FrustumVertex().fromLerp(this.vertices.near[j], this.vertices.far[j], breaks[i])); + } + } + + result.push(cascade); + } + + return result; + } + + toSpace(cameraMatrix) { + const result = new Frustum(); + const point = new Vector3(); + + for(var i = 0; i < 4; i++) { + point.set(this.vertices.near[i].x, this.vertices.near[i].y, this.vertices.near[i].z); + point.applyMatrix4(cameraMatrix); + result.vertices.near.push(new FrustumVertex(point.x, point.y, point.z)); + + point.set(this.vertices.far[i].x, this.vertices.far[i].y, this.vertices.far[i].z); + point.applyMatrix4(cameraMatrix); + result.vertices.far.push(new FrustumVertex(point.x, point.y, point.z)); + } + + return result; + } +} + +class FrustumBoundingBox { + constructor() { + this.min = { + x: 0, + y: 0, + z: 0 + }; + this.max = { + x: 0, + y: 0, + z: 0 + }; + } + + fromFrustum(frustum) { + const vertices = []; + + for(let i = 0; i < 4; i++) { + vertices.push(frustum.vertices.near[i]); + vertices.push(frustum.vertices.far[i]); + } + + this.min = { + x: vertices[0].x, + y: vertices[0].y, + z: vertices[0].z + }; + this.max = { + x: vertices[0].x, + y: vertices[0].y, + z: vertices[0].z + }; + + for(let i = 1; i < 8; i++) { + this.min.x = Math.min(this.min.x, vertices[i].x); + this.min.y = Math.min(this.min.y, vertices[i].y); + this.min.z = Math.min(this.min.z, vertices[i].z); + this.max.x = Math.max(this.max.x, vertices[i].x); + this.max.y = Math.max(this.max.y, vertices[i].y); + this.max.z = Math.max(this.max.z, vertices[i].z); + } + + return this; + } + + getSize() { + this.size = { + x: this.max.x - this.min.x, + y: this.max.y - this.min.y, + z: this.max.z - this.min.z + }; + + return this.size; + } + + getCenter(margin) { + this.center = { + x: (this.max.x + this.min.x) / 2, + y: (this.max.y + this.min.y) / 2, + z: this.max.z + margin + }; + + return this.center; + } +} + +var Shader = { + lights_fragment_begin: ` +GeometricContext geometry; +geometry.position = - vViewPosition; +geometry.normal = normal; +geometry.viewDir = normalize( vViewPosition ); +#ifdef CLEARCOAT + geometry.clearcoatNormal = clearcoatNormal; +#endif +IncidentLight directLight; +#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct ) + PointLight pointLight; + #pragma unroll_loop + for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) { + pointLight = pointLights[ i ]; + getPointDirectLightIrradiance( pointLight, geometry, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) + directLight.color *= all( bvec3( pointLight.shadow, directLight.visible, receiveShadow ) ) ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0; + #endif + RE_Direct( directLight, geometry, material, reflectedLight ); + } +#endif +#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct ) + SpotLight spotLight; + #pragma unroll_loop + for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) { + spotLight = spotLights[ i ]; + getSpotDirectLightIrradiance( spotLight, geometry, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + directLight.color *= all( bvec3( spotLight.shadow, directLight.visible, receiveShadow ) ) ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowBias, spotLight.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0; + #endif + RE_Direct( directLight, geometry, material, reflectedLight ); + } +#endif +#if ( NUM_DIR_LIGHTS > 0) && defined( RE_Direct ) && defined( USE_CSM ) && defined( CSM_CASCADES ) + DirectionalLight directionalLight; + float linearDepth = (vViewPosition.z) / (shadowFar - cameraNear); + + #pragma unroll_loop + for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { + directionalLight = directionalLights[ i ]; + getDirectionalDirectLightIrradiance( directionalLight, geometry, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) + if(linearDepth >= CSM_cascades[UNROLLED_LOOP_INDEX].x && linearDepth < CSM_cascades[UNROLLED_LOOP_INDEX].y) directLight.color *= all( bvec3( directionalLight.shadow, directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; + #endif + if(linearDepth >= CSM_cascades[UNROLLED_LOOP_INDEX].x && (linearDepth < CSM_cascades[UNROLLED_LOOP_INDEX].y || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1)) RE_Direct( directLight, geometry, material, reflectedLight ); + } +#endif +#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) && !defined( USE_CSM ) && !defined( CSM_CASCADES ) + DirectionalLight directionalLight; + #pragma unroll_loop + for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { + directionalLight = directionalLights[ i ]; + getDirectionalDirectLightIrradiance( directionalLight, geometry, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) + directLight.color *= all( bvec2( directionalLight.shadow, directLight.visible ) ) ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; + #endif + RE_Direct( directLight, geometry, material, reflectedLight ); + } +#endif +#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea ) + RectAreaLight rectAreaLight; + #pragma unroll_loop + for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) { + rectAreaLight = rectAreaLights[ i ]; + RE_Direct_RectArea( rectAreaLight, geometry, material, reflectedLight ); + } +#endif +#if defined( RE_IndirectDiffuse ) + vec3 iblIrradiance = vec3( 0.0 ); + vec3 irradiance = getAmbientLightIrradiance( ambientLightColor ); + irradiance += getLightProbeIrradiance( lightProbe, geometry ); + #if ( NUM_HEMI_LIGHTS > 0 ) + #pragma unroll_loop + for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) { + irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry ); + } + #endif +#endif +#if defined( RE_IndirectSpecular ) + vec3 radiance = vec3( 0.0 ); + vec3 clearcoatRadiance = vec3( 0.0 ); +#endif +`, + lights_pars_begin: ` +#if defined( USE_CSM ) && defined( CSM_CASCADES ) +uniform vec2 CSM_cascades[CSM_CASCADES]; +uniform float cameraNear; +uniform float shadowFar; +#endif + ` + ShaderChunk.lights_pars_begin +}; + +class CSM { + constructor(data) { + data = data || {}; + + this.camera = data.camera; + this.parent = data.parent; + this.fov = data.fov || this.camera.fov; + this.near = this.camera.near; + this.far = data.far || this.camera.far; + this.aspect = data.aspect || this.camera.aspect; + this.cascades = data.cascades || 3; + this.mode = data.mode || 'practical'; + this.shadowMapSize = data.shadowMapSize || 2048; + this.shadowBias = data.shadowBias || 0.000001; + this.lightDirection = data.lightDirection || new Vector3(1, -1, 1).normalize(); + this.lightIntensity = data.lightIntensity || 1; + this.lightNear = data.lightNear || 1; + this.lightFar = data.lightFar || 2000; + this.lightMargin = data.lightMargin || 200; + this.customSplitsCallback = data.customSplitsCallback; + + this.lights = []; + this.materials = []; + this.createLights(); + + this.getBreaks(); + this.initCascades(); + + this.injectInclude(); + } + + createLights() { + for(let i = 0; i < this.cascades; i++) { + const light = new DirectionalLight(0xffffff, this.lightIntensity); + light.castShadow = true; + light.shadow.mapSize.width = this.shadowMapSize; + light.shadow.mapSize.height = this.shadowMapSize; + + light.shadow.camera.near = this.lightNear; + light.shadow.camera.far = this.lightFar; + light.shadow.bias = this.shadowBias; + + this.parent.add(light); + this.parent.add(light.target); + this.lights.push(light); + } + } + + initCascades() { + this.mainFrustum = new Frustum({ + fov: this.fov, + near: this.near, + far: this.far, + aspect: this.aspect + }); + + this.mainFrustum.getViewSpaceVertices(); + + this.frustums = this.mainFrustum.split(this.breaks); + } + + getBreaks() { + this.breaks = []; + + switch (this.mode) { + case 'uniform': + this.breaks = uniformSplit(this.cascades, this.near, this.far); + break; + case 'logarithmic': + this.breaks = logarithmicSplit(this.cascades, this.near, this.far); + break; + case 'practical': + this.breaks = practicalSplit(this.cascades, this.near, this.far, 0.5); + break; + case 'custom': + if(this.customSplitsCallback === undefined) console.error('CSM: Custom split scheme callback not defined.'); + this.breaks = this.customSplitsCallback(this.cascades, this.near, this.far); + break; + } + + function uniformSplit(amount, near, far) { + const r = []; + + for(let i = 1; i < amount; i++) { + r.push((near + (far - near) * i / amount) / far); + } + + r.push(1); + return r; + } + + function logarithmicSplit(amount, near, far) { + const r = []; + + for(let i = 1; i < amount; i++) { + r.push((near * (far / near) ** (i / amount)) / far); + } + + r.push(1); + return r; + } + + function practicalSplit(amount, near, far, lambda) { + const log = logarithmicSplit(amount, near, far); + const uni = uniformSplit(amount, near, far); + const r = []; + + for(let i = 1; i < amount; i++) { + r.push(lambda * log[i - 1] + (1 - lambda) * uni[i - 1]); + } + + r.push(1); + return r; + } + } + + update(cameraMatrix) { + for(let i = 0; i < this.frustums.length; i++) { + const worldSpaceFrustum = this.frustums[i].toSpace(cameraMatrix); + const light = this.lights[i]; + const lightSpaceFrustum = worldSpaceFrustum.toSpace(light.shadow.camera.matrixWorldInverse); + + light.shadow.camera.updateMatrixWorld(true); + + const bbox = new FrustumBoundingBox().fromFrustum(lightSpaceFrustum); + bbox.getSize(); + bbox.getCenter(this.lightMargin); + + const squaredBBWidth = Math.max(bbox.size.x, bbox.size.y); + + let center = new Vector3(bbox.center.x, bbox.center.y, bbox.center.z); + center.applyMatrix4(light.shadow.camera.matrixWorld); + + light.shadow.camera.left = -squaredBBWidth / 2; + light.shadow.camera.right = squaredBBWidth / 2; + light.shadow.camera.top = squaredBBWidth / 2; + light.shadow.camera.bottom = -squaredBBWidth / 2; + + light.position.copy(center); + light.target.position.copy(center); + + light.target.position.x += this.lightDirection.x; + light.target.position.y += this.lightDirection.y; + light.target.position.z += this.lightDirection.z; + + light.shadow.camera.updateProjectionMatrix(); + light.shadow.camera.updateMatrixWorld(); + } + } + + injectInclude() { + ShaderChunk.lights_fragment_begin = Shader.lights_fragment_begin; + ShaderChunk.lights_pars_begin = Shader.lights_pars_begin; + } + + setupMaterial(material) { + material.defines = material.defines || {}; + material.defines.USE_CSM = 1; + material.defines.CSM_CASCADES = this.cascades; + + const breaksVec2 = []; + + for(let i = 0; i < this.cascades; i++) { + let amount = this.breaks[i]; + let prev = this.breaks[i - 1] || 0; + breaksVec2.push(new Vector2(prev, amount)); + } + + const self = this; + + material.onBeforeCompile = function (shader) { + shader.uniforms.CSM_cascades = {value: breaksVec2}; + shader.uniforms.cameraNear = {value: self.camera.near}; + shader.uniforms.shadowFar = {value: self.far}; + + self.materials.push(shader); + }; + } + + updateUniforms() { + for(let i = 0; i < this.materials.length; i++) { + this.materials[i].uniforms.CSM_cascades.value = this.getExtendedBreaks(); + this.materials[i].uniforms.cameraNear.value = this.camera.near; + this.materials[i].uniforms.shadowFar.value = this.far; + } + } + + getExtendedBreaks() { + let breaksVec2 = []; + + for(let i = 0; i < this.cascades; i++) { + let amount = this.breaks[i]; + let prev = this.breaks[i - 1] || 0; + breaksVec2.push(new Vector2(prev, amount)); + } + + return breaksVec2; + } + + setAspect(aspect) { + this.aspect = aspect; + this.initCascades(); + } + + updateFrustums() { + this.getBreaks(); + this.initCascades(); + this.updateUniforms(); + } + + helper(cameraMatrix) { + let frustum; + let geometry; + const material = new LineBasicMaterial({color: 0xffffff}); + const object = new Object3D(); + + for(let i = 0; i < this.frustums.length; i++) { + frustum = this.frustums[i].toSpace(cameraMatrix); + + geometry = new Geometry(); + + for(let i = 0; i < 5; i++) { + const point = frustum.vertices.near[i === 4 ? 0 : i]; + geometry.vertices.push(new Vector3(point.x, point.y, point.z)); + } + + object.add(new Line(geometry, material)); + + geometry = new Geometry(); + + for(let i = 0; i < 5; i++) { + const point = frustum.vertices.far[i === 4 ? 0 : i]; + geometry.vertices.push(new Vector3(point.x, point.y, point.z)); + } + + object.add(new Line(geometry, material)); + + for(let i = 0; i < 4; i++) { + geometry = new Geometry(); + + const near = frustum.vertices.near[i]; + const far = frustum.vertices.far[i]; + + geometry.vertices.push(new Vector3(near.x, near.y, near.z)); + geometry.vertices.push(new Vector3(far.x, far.y, far.z)); + + object.add(new Line(geometry, material)); + } + } + + return object; + } + + remove() { + for(let i = 0; i < this.lights.length; i++) { + this.parent.remove(this.lights[i]); + } + } +} + +export default CSM; diff --git a/examples/webgl_cascadedshadowmaps.html b/examples/webgl_cascadedshadowmaps.html new file mode 100644 index 0000000000000000000000000000000000000000..0c5ddd5ae1df3f9b3bcfa691332bb4601eeebba1 --- /dev/null +++ b/examples/webgl_cascadedshadowmaps.html @@ -0,0 +1,205 @@ + + + + three.js webgl - cascaded shadow maps + + + + + + +
+
+ three.js webgl - cascaded shadow maps
+ by vtHawk (original repository) +
+ + + + +