提交 e1198317 编写于 作者: V vtHawk

CSM example

上级 a3ca9ba6
......@@ -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",
......
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;
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - cascaded shadow maps</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="container"></div>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - cascaded shadow maps<br>
by <a href="https://github.com/vtHawk" target="_blank" rel="noopener">vtHawk</a> (<a href="https://github.com/vtHawk/three-csm" target="_blank" rel="noopener">original repository</a>)
</div>
<script type="module">
import * as THREE from '../build/three.module.js';
import { OrbitControls } from './jsm/controls/OrbitControls.js';
import { GUI } from './jsm/libs/dat.gui.module.js';
import * as CSM from './js/libs/three-csm.module.js';
var renderer, scene, camera, controls, csm;
init();
animate();
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color( '#454e61' );
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 5000 );
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
controls = new OrbitControls( camera, renderer.domElement );
controls.maxPolarAngle = Math.PI / 2;
camera.position.set( 60, 60, 0 );
controls.target = new THREE.Vector3( -100, 10, 0 );
controls.update();
var ambientLight = new THREE.AmbientLight( 0xffffff, 0.5 );
scene.add( ambientLight );
var params = {
far: 1000,
mode: 'practical',
lightX: - 1,
lightY: - 1,
lightZ: - 1,
margin: 100,
lightFar: 5000,
lightNear: 1,
helper: function () {
var helper = csm.helper( camera.matrix );
scene.add( helper );
}
};
csm = new CSM.default({
fov: camera.fov,
near: camera.near,
far: params.far,
aspect: camera.aspect,
cascades: 4,
mode: params.mode,
parent: scene,
shadowMapSize: 1024,
lightDirection: new THREE.Vector3( params.lightX, params.lightY, params.lightZ ).normalize(),
camera: camera
} );
var floorMaterial = new THREE.MeshPhongMaterial( { color: '#252a34' } );
csm.setupMaterial( floorMaterial );
var floor = new THREE.Mesh( new THREE.PlaneBufferGeometry( 10000, 10000 ), floorMaterial );
floor.rotation.x = - Math.PI / 2;
floor.castShadow = true;
floor.receiveShadow = true;
scene.add( floor );
var material1 = new THREE.MeshPhongMaterial( { color: '#08d9d6' } );
csm.setupMaterial( material1 );
var material2 = new THREE.MeshPhongMaterial( { color: '#ff2e63' } );
csm.setupMaterial( material2 );
var geometry = new THREE.BoxBufferGeometry( 10, 10, 10 );
for ( var i = 0; i < 40; i ++ ) {
var cube1 = new THREE.Mesh( geometry, i % 2 === 0 ? material1 : material2 );
cube1.castShadow = true;
cube1.receiveShadow = true;
scene.add( cube1 );
cube1.position.set( - i * 25, 20, 30 );
cube1.scale.y = Math.random() * 2 + 6;
var cube2 = new THREE.Mesh( geometry, i % 2 === 0 ? material2 : material1 );
cube2.castShadow = true;
cube2.receiveShadow = true;
scene.add( cube2 );
cube2.position.set( - i * 25, 20, - 30 );
cube2.scale.y = Math.random() * 2 + 6;
}
var gui = new GUI();
gui.add( params, 'far', 1, 5000 ).step( 1 ).name( 'shadow far' ).onChange( function ( value ) {
csm.far = value;
csm.updateFrustums();
} );
gui.add( params, 'mode', [ 'uniform', 'logarithmic', 'practical' ] ).name( 'frustum split mode' ).onChange( function ( value ) {
csm.mode = value;
csm.updateFrustums();
} );
gui.add( params, 'lightX', - 1, 1 ).name( 'light direction x' ).onChange( function ( value ) {
csm.lightDirection.x = value;
} );
gui.add( params, 'lightY', - 1, 1 ).name( 'light direction y' ).onChange( function ( value ) {
csm.lightDirection.y = value;
} );
gui.add( params, 'lightZ', - 1, 1 ).name( 'light direction z' ).onChange( function ( value ) {
csm.lightDirection.z = value;
} );
gui.add( params, 'margin', 0, 200 ).name( 'light margin' ).onChange( function ( value ) {
csm.lightMargin = value;
} );
gui.add( params, 'lightNear', 1, 10000 ).name( 'light near' ).onChange( function ( value ) {
for ( var i = 0; i < csm.lights.length; i ++ ) {
csm.lights[ i ].shadow.camera.near = value;
csm.lights[ i ].shadow.camera.updateProjectionMatrix();
}
} );
gui.add( params, 'lightFar', 1, 10000 ).name( 'light far' ).onChange( function ( value ) {
for ( var i = 0; i < csm.lights.length; i ++ ) {
csm.lights[ i ].shadow.camera.far = value;
csm.lights[ i ].shadow.camera.updateProjectionMatrix();
}
} );
gui.add( params, 'helper' ).name( 'add frustum helper' );
window.addEventListener( 'resize', function () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
csm.setAspect( camera.aspect );
renderer.setSize( window.innerWidth, window.innerHeight );
}, false);
}
function animate() {
requestAnimationFrame( animate );
csm.update( camera.matrix );
controls.update();
renderer.render( scene, camera );
}
</script>
</body>
</html>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册