@navaramap/three-csm 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ // Based on the following work:
2
+ // https://github.com/StrandedKitty/three-csm/tree/master/src
3
+ // https://github.com/mrdoob/three.js/tree/r169/examples/jsm/csm
4
+
5
+ import { type Material, type WebGLProgramParametersWithUniforms } from "three";
6
+
7
+ import { type CascadedShadowMaps } from "./CascadedShadowMaps";
8
+ import { createFragmentShader } from "./createFragmentShader";
9
+
10
+ type OnBeforeCompile = Material["onBeforeCompile"];
11
+
12
+ export type MaterialState = {
13
+ parameters?: WebGLProgramParametersWithUniforms;
14
+ originalHandler?: OnBeforeCompile;
15
+ };
16
+
17
+ export class MaterialStates {
18
+ private readonly map = new Map<Material, MaterialState>();
19
+
20
+ setup<T extends Material>(material: T, csm: CascadedShadowMaps): T {
21
+ if (this.map.has(material)) {
22
+ return material;
23
+ }
24
+
25
+ material.userData.defines ??= {};
26
+ material.userData.defines.CSM = 1;
27
+ material.userData.defines.CSM_CASCADE_COUNT = csm.cascadeCount;
28
+ if (csm.fade) {
29
+ material.userData.defines.CSM_FADE = 1;
30
+ }
31
+
32
+ const stateRef: MaterialState = {};
33
+ this.map.set(material, stateRef);
34
+
35
+ const handleBeforeCompile: OnBeforeCompile = (parameters) => {
36
+ stateRef.parameters = parameters;
37
+
38
+ const cascades = csm.cascades;
39
+ const near = csm.mainCamera.near;
40
+ const far = Math.min(csm.mainCamera.far, csm.far);
41
+ const uniforms = parameters.uniforms;
42
+ uniforms.csmCascades = { value: cascades };
43
+ uniforms.csmNear = { value: near };
44
+ uniforms.csmFar = { value: far };
45
+
46
+ parameters.fragmentShader = createFragmentShader(
47
+ parameters.fragmentShader,
48
+ );
49
+ };
50
+
51
+ const originalHandler = material.onBeforeCompile.bind(material);
52
+ const originalCustomProgramCacheKey =
53
+ material.customProgramCacheKey.bind(material);
54
+ if (Object.prototype.hasOwnProperty.call(material, "hasOwnProperty")) {
55
+ stateRef.originalHandler = originalHandler;
56
+ }
57
+ material.customProgramCacheKey = () =>
58
+ originalHandler.toString() + originalCustomProgramCacheKey();
59
+ material.onBeforeCompile = (...args) => {
60
+ const [shader] = args;
61
+ shader.defines ??= {};
62
+ Object.assign(shader.defines, material.userData.defines);
63
+ originalHandler.apply(material, args);
64
+ handleBeforeCompile(...args);
65
+ };
66
+
67
+ const handleDispose = (): void => {
68
+ // Just discard it from our state map without rolling it back.
69
+ this.map.delete(material);
70
+ material.removeEventListener("dispose", handleDispose);
71
+ };
72
+ material.addEventListener("dispose", handleDispose);
73
+
74
+ return material;
75
+ }
76
+
77
+ update(csm: CascadedShadowMaps): void {
78
+ const cascadeCount = csm.cascadeCount;
79
+ const fade = csm.fade;
80
+ const cascades = csm.cascades;
81
+ const near = csm.mainCamera.near;
82
+ const far = Math.min(csm.mainCamera.far, csm.far);
83
+
84
+ for (const [material, state] of this.map.entries()) {
85
+ const defines = material.userData.defines;
86
+ if (defines != null) {
87
+ let needsUpdate = false;
88
+ if ((defines.CSM_FADE != null) !== fade) {
89
+ if (fade) {
90
+ defines.CSM_FADE = 1;
91
+ } else {
92
+ delete defines.CSM_FADE;
93
+ }
94
+ needsUpdate = true;
95
+ }
96
+ if (defines.CSM_CASCADE_COUNT !== cascadeCount) {
97
+ defines.CSM_CASCADE_COUNT = cascadeCount;
98
+ needsUpdate = true;
99
+ }
100
+ if (needsUpdate) {
101
+ material.needsUpdate = true;
102
+ }
103
+ }
104
+
105
+ const uniforms = state.parameters?.uniforms;
106
+ if (uniforms != null) {
107
+ uniforms.csmCascades.value = cascades;
108
+ uniforms.csmNear.value = near;
109
+ uniforms.csmFar.value = far;
110
+ }
111
+ }
112
+ }
113
+
114
+ rollback<T extends Material>(material: T): T {
115
+ const state = this.map.get(material);
116
+ return state != null ? this.rollbackToState(material, state) : material;
117
+ }
118
+
119
+ private rollbackToState<T extends Material>(
120
+ material: T,
121
+ { parameters, originalHandler }: MaterialState,
122
+ ): T {
123
+ if (parameters != null) {
124
+ // TODO: Do we really need to delete uniforms when materials are going to
125
+ // be updated anyways?
126
+ delete parameters.uniforms.csmCascades;
127
+ delete parameters.uniforms.csmNear;
128
+ delete parameters.uniforms.csmFar;
129
+ }
130
+ if (originalHandler != null) {
131
+ material.onBeforeCompile = originalHandler;
132
+ } else {
133
+ // @ts-expect-error Restore the prototype function by deleting the handler
134
+ // we've assigned above.
135
+ delete material.onBeforeCompile;
136
+ }
137
+ if (material.userData.defines != null) {
138
+ delete material.userData.defines.CSM;
139
+ delete material.userData.defines.CSM_CASCADE_COUNT;
140
+ delete material.userData.defines.CSM_FADE;
141
+ }
142
+ material.needsUpdate = true;
143
+
144
+ this.map.delete(material);
145
+ return material;
146
+ }
147
+
148
+ dispose(): void {
149
+ for (const [material, state] of this.map) {
150
+ this.rollbackToState(material, state);
151
+ }
152
+ this.map.clear();
153
+ }
154
+ }
@@ -0,0 +1,92 @@
1
+ import { ShaderChunk } from "three";
2
+
3
+ import cascadedFadedLights from "./shaders/cascadedFadedLights.glsl?raw";
4
+ import cascadedLights from "./shaders/cascadedLights.glsl?raw";
5
+ import cascadedLightsWithoutShadows from "./shaders/cascadedLightsWithoutShadows.glsl?raw";
6
+ import defaultLights from "./shaders/defaultLights.glsl?raw";
7
+ import nonShadowCastingLights from "./shaders/nonShadowCastingLights.glsl?raw";
8
+
9
+ const uniforms = /* glsl */ `
10
+ #include <lights_pars_begin>
11
+
12
+ #ifdef CSM
13
+ uniform vec2 csmCascades[CSM_CASCADE_COUNT];
14
+ uniform float csmNear;
15
+ uniform float csmFar;
16
+ #endif // CSM
17
+ `;
18
+
19
+ function body(shader: string): string {
20
+ return shader.replace(/^\s*void\s+\w+\s*\(\s*\)\s*/m, "");
21
+ }
22
+
23
+ const directionalLights = /*#__PURE__*/ (() => /* glsl */ `
24
+ #if NUM_DIR_LIGHTS > 0 && defined(RE_Direct)
25
+ DirectionalLight directionalLight;
26
+ #if defined(USE_SHADOWMAP) && NUM_DIR_LIGHT_SHADOWS > 0
27
+ DirectionalLightShadow directionalLightShadow;
28
+ #endif
29
+
30
+ #ifdef CSM
31
+ #ifdef USE_SHADOWMAP
32
+ #ifdef CSM_FADE
33
+ ${body(cascadedFadedLights)}
34
+ #else
35
+ ${body(cascadedLights)}
36
+ #endif // CSM_FADE
37
+ #elif NUM_DIR_LIGHT_SHADOWS > 0
38
+ ${body(cascadedLightsWithoutShadows)}
39
+ #endif // USE_SHADOWMAP
40
+
41
+ #if NUM_DIR_LIGHTS > NUM_DIR_LIGHT_SHADOWS
42
+ ${body(nonShadowCastingLights)}
43
+ #endif
44
+ #else
45
+ ${body(defaultLights)}
46
+ #endif // CSM
47
+ #endif // NUM_DIR_LIGHTS > 0 && defined(RE_Direct)
48
+ `)();
49
+
50
+ type ReplaceRangeParams = {
51
+ source: string;
52
+ startLine: string;
53
+ endLine: string;
54
+ replacement: string;
55
+ };
56
+
57
+ function findAndReplace({
58
+ source,
59
+ startLine,
60
+ endLine,
61
+ replacement,
62
+ }: ReplaceRangeParams): string {
63
+ const startText = `\n${startLine}\n`;
64
+ const startIndex = source.indexOf(startText);
65
+ if (startIndex < 0) {
66
+ return source;
67
+ }
68
+ const endText = `\n${endLine}\n`;
69
+ const endIndex = source.indexOf(endText, startIndex + startText.length);
70
+ if (endIndex < 0) {
71
+ return source;
72
+ }
73
+ return (
74
+ source.slice(0, startIndex + 1) +
75
+ replacement +
76
+ source.slice(endIndex + endText.length - 1)
77
+ );
78
+ }
79
+
80
+ export function createFragmentShader(fragmentShader: string): string {
81
+ return fragmentShader
82
+ .replace("#include <lights_pars_begin>", uniforms)
83
+ .replace(
84
+ "#include <lights_fragment_begin>",
85
+ findAndReplace({
86
+ source: ShaderChunk.lights_fragment_begin,
87
+ startLine: "#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )",
88
+ endLine: "#endif",
89
+ replacement: directionalLights,
90
+ }),
91
+ );
92
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./CascadedDirectionalLights";
2
+ export * from "./CascadedShadowMaps";
3
+ export * from "./CSMHelper";
4
+ export * from "./splitFrustum";
5
+ export { createFragmentShader as overrideFragmentShaderForCSM } from "./createFragmentShader";
@@ -0,0 +1,102 @@
1
+ // Based on the following work:
2
+ // https://github.com/StrandedKitty/three-csm/tree/master/src
3
+ // https://github.com/mrdoob/three.js/tree/r169/examples/jsm/csm
4
+
5
+ void cascadedFadedLights() {
6
+ directionalLight = directionalLights[0];
7
+ getDirectionalLightInfo(directionalLight, directLight);
8
+
9
+ float clipDepth = vViewPosition.z / (csmFar - csmNear);
10
+ bool lastCascade;
11
+ vec2 cascade;
12
+ float cascadeCenter;
13
+ float closestEdge;
14
+ float margin;
15
+ float shadow = 1.0;
16
+ float nextShadow = 1.0;
17
+
18
+ #pragma unroll_loop_start
19
+ for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
20
+ #if UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
21
+ #if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT
22
+
23
+ lastCascade = UNROLLED_LOOP_INDEX == CSM_CASCADE_COUNT - 1;
24
+ cascade = csmCascades[i];
25
+ cascadeCenter = (cascade.x + cascade.y) * 0.5;
26
+ closestEdge = clipDepth < cascadeCenter ? cascade.x : cascade.y;
27
+ margin = 0.25 * pow(closestEdge, 2.0);
28
+ cascade += margin * vec2(-0.5, 0.5);
29
+
30
+ if (clipDepth >= cascade.x && (clipDepth < cascade.y || lastCascade)) {
31
+ directionalLightShadow = directionalLightShadows[i];
32
+ float prevShadow = nextShadow;
33
+ nextShadow =
34
+ directLight.visible && receiveShadow
35
+ ? getShadow(
36
+ directionalShadowMap[i],
37
+ directionalLightShadow.shadowMapSize,
38
+ directionalLightShadow.shadowIntensity,
39
+ directionalLightShadow.shadowBias,
40
+ directionalLightShadow.shadowRadius,
41
+ vDirectionalShadowCoord[i]
42
+ )
43
+ : 1.0;
44
+
45
+ shadow = mix(
46
+ prevShadow,
47
+ nextShadow,
48
+ saturate(min(clipDepth - cascade.x, cascade.y - clipDepth) / margin)
49
+ );
50
+ }
51
+ #endif // UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT
52
+ #endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
53
+ }
54
+ #pragma unroll_loop_end
55
+
56
+ // No loop here: all cascaded lights are in fact one light only.
57
+ directLight.color *= shadow;
58
+ RE_Direct(
59
+ directLight,
60
+ geometryPosition,
61
+ geometryNormal,
62
+ geometryViewDir,
63
+ geometryClearcoatNormal,
64
+ material,
65
+ reflectedLight
66
+ );
67
+
68
+ // The rest of directional lights.
69
+ #pragma unroll_loop_start
70
+ for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
71
+ #if UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
72
+ #if UNROLLED_LOOP_INDEX >= CSM_CASCADE_COUNT
73
+ directionalLight = directionalLights[i];
74
+ getDirectionalLightInfo(directionalLight, directLight);
75
+
76
+ directionalLightShadow = directionalLightShadows[i];
77
+ directLight.color *=
78
+ directLight.visible && receiveShadow
79
+ ? getShadow(
80
+ directionalShadowMap[i],
81
+ directionalLightShadow.shadowMapSize,
82
+ directionalLightShadow.shadowIntensity,
83
+ directionalLightShadow.shadowBias,
84
+ directionalLightShadow.shadowRadius,
85
+ vDirectionalShadowCoord[i]
86
+ )
87
+ : 1.0;
88
+
89
+ RE_Direct(
90
+ directLight,
91
+ geometryPosition,
92
+ geometryNormal,
93
+ geometryViewDir,
94
+ geometryClearcoatNormal,
95
+ material,
96
+ reflectedLight
97
+ );
98
+ #endif // UNROLLED_LOOP_INDEX >= CSM_CASCADE_COUNT
99
+ #endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
100
+ }
101
+ #pragma unroll_loop_end
102
+ }
@@ -0,0 +1,81 @@
1
+ // Based on the following work:
2
+ // https://github.com/StrandedKitty/three-csm/tree/master/src
3
+ // https://github.com/mrdoob/three.js/tree/r169/examples/jsm/csm
4
+
5
+ void cascadedLights() {
6
+ directionalLight = directionalLights[0];
7
+ getDirectionalLightInfo(directionalLight, directLight);
8
+
9
+ float clipDepth = vViewPosition.z / (csmFar - csmNear);
10
+ vec2 cascade;
11
+
12
+ #pragma unroll_loop_start
13
+ for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
14
+ #if UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
15
+ #if UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT
16
+ cascade = csmCascades[i];
17
+ directionalLightShadow = directionalLightShadows[i];
18
+ if (clipDepth >= cascade.x && clipDepth < cascade.y) {
19
+ directLight.color *=
20
+ directLight.visible && receiveShadow
21
+ ? getShadow(
22
+ directionalShadowMap[i],
23
+ directionalLightShadow.shadowMapSize,
24
+ directionalLightShadow.shadowIntensity,
25
+ directionalLightShadow.shadowBias,
26
+ directionalLightShadow.shadowRadius,
27
+ vDirectionalShadowCoord[i]
28
+ )
29
+ : 1.0;
30
+ }
31
+ #endif // UNROLLED_LOOP_INDEX < CSM_CASCADE_COUNT
32
+ #endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
33
+ }
34
+ #pragma unroll_loop_end
35
+
36
+ // No loop here: all cascaded lights are in fact one light only.
37
+ RE_Direct(
38
+ directLight,
39
+ geometryPosition,
40
+ geometryNormal,
41
+ geometryViewDir,
42
+ geometryClearcoatNormal,
43
+ material,
44
+ reflectedLight
45
+ );
46
+
47
+ // The rest of directional lights.
48
+ #pragma unroll_loop_start
49
+ for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
50
+ #if UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
51
+ #if UNROLLED_LOOP_INDEX >= CSM_CASCADE_COUNT
52
+ directionalLight = directionalLights[i];
53
+ getDirectionalLightInfo(directionalLight, directLight);
54
+
55
+ directionalLightShadow = directionalLightShadows[i];
56
+ directLight.color *=
57
+ directLight.visible && receiveShadow
58
+ ? getShadow(
59
+ directionalShadowMap[i],
60
+ directionalLightShadow.shadowMapSize,
61
+ directionalLightShadow.shadowIntensity,
62
+ directionalLightShadow.shadowBias,
63
+ directionalLightShadow.shadowRadius,
64
+ vDirectionalShadowCoord[i]
65
+ )
66
+ : 1.0;
67
+
68
+ RE_Direct(
69
+ directLight,
70
+ geometryPosition,
71
+ geometryNormal,
72
+ geometryViewDir,
73
+ geometryClearcoatNormal,
74
+ material,
75
+ reflectedLight
76
+ );
77
+ #endif // UNROLLED_LOOP_INDEX >= CSM_CASCADE_COUNT
78
+ #endif // UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
79
+ }
80
+ #pragma unroll_loop_end
81
+ }
@@ -0,0 +1,35 @@
1
+ // Based on the following work:
2
+ // https://github.com/StrandedKitty/three-csm/tree/master/src
3
+ // https://github.com/mrdoob/three.js/tree/r169/examples/jsm/csm
4
+
5
+ void cascadedLightsWithoutShadows() {
6
+ // No loop here: all cascaded lights are in fact one light only.
7
+ getDirectionalLightInfo(directionalLights[0], directLight);
8
+ RE_Direct(
9
+ directLight,
10
+ geometryPosition,
11
+ geometryNormal,
12
+ geometryViewDir,
13
+ geometryClearcoatNormal,
14
+ material,
15
+ reflectedLight
16
+ );
17
+
18
+ // The rest of directional lights.
19
+ #pragma unroll_loop_start
20
+ for (int i = CSM_CASCADE_COUNT; i < NUM_DIR_LIGHTS; i++) {
21
+ directionalLight = directionalLights[i];
22
+ getDirectionalLightInfo(directionalLight, directLight);
23
+
24
+ RE_Direct(
25
+ directLight,
26
+ geometryPosition,
27
+ geometryNormal,
28
+ geometryViewDir,
29
+ geometryClearcoatNormal,
30
+ material,
31
+ reflectedLight
32
+ );
33
+ }
34
+ #pragma unroll_loop_end
35
+ }
@@ -0,0 +1,36 @@
1
+ // Default shader.
2
+ // See: https://github.com/mrdoob/three.js/blob/r169/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js#L136
3
+
4
+ void defaultLights() {
5
+ #pragma unroll_loop_start
6
+ for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
7
+ directionalLight = directionalLights[i];
8
+ getDirectionalLightInfo(directionalLight, directLight);
9
+
10
+ #if defined(USE_SHADOWMAP) && UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS
11
+ directionalLightShadow = directionalLightShadows[i];
12
+ directLight.color *=
13
+ directLight.visible && receiveShadow
14
+ ? getShadow(
15
+ directionalShadowMap[i],
16
+ directionalLightShadow.shadowMapSize,
17
+ directionalLightShadow.shadowIntensity,
18
+ directionalLightShadow.shadowBias,
19
+ directionalLightShadow.shadowRadius,
20
+ vDirectionalShadowCoord[i]
21
+ )
22
+ : 1.0;
23
+ #endif
24
+
25
+ RE_Direct(
26
+ directLight,
27
+ geometryPosition,
28
+ geometryNormal,
29
+ geometryViewDir,
30
+ geometryClearcoatNormal,
31
+ material,
32
+ reflectedLight
33
+ );
34
+ }
35
+ #pragma unroll_loop_end
36
+ }
@@ -0,0 +1,18 @@
1
+ void nonShadowCastingLights() {
2
+ #pragma unroll_loop_start
3
+ for (int i = NUM_DIR_LIGHT_SHADOWS; i < NUM_DIR_LIGHTS; i++) {
4
+ directionalLight = directionalLights[i];
5
+ getDirectionalLightInfo(directionalLight, directLight);
6
+
7
+ RE_Direct(
8
+ directLight,
9
+ geometryPosition,
10
+ geometryNormal,
11
+ geometryViewDir,
12
+ geometryClearcoatNormal,
13
+ material,
14
+ reflectedLight
15
+ );
16
+ }
17
+ #pragma unroll_loop_end
18
+ }
@@ -0,0 +1,59 @@
1
+ import { lerp } from "three/src/math/MathUtils.js";
2
+
3
+ export type FrustumSplitFunction = (
4
+ length: number,
5
+ near: number,
6
+ far: number,
7
+ lambda?: number,
8
+ result?: number[],
9
+ ) => number[];
10
+
11
+ export type FrustumSplitFunctions = {
12
+ uniform: FrustumSplitFunction;
13
+ logarithmic: FrustumSplitFunction;
14
+ practical: FrustumSplitFunction;
15
+ };
16
+
17
+ export type FrustumSplitMode = keyof FrustumSplitFunctions;
18
+
19
+ // See: https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-10-parallel-split-shadow-maps-programmable-gpus
20
+ const modes: FrustumSplitFunctions = {
21
+ uniform: (count, near, far, _, result = []) => {
22
+ for (let i = 0; i < count; ++i) {
23
+ result[i] = (near + ((far - near) * (i + 1)) / count) / far;
24
+ }
25
+ result.length = count;
26
+ return result;
27
+ },
28
+
29
+ logarithmic: (count, near, far, _, result = []) => {
30
+ for (let i = 0; i < count; ++i) {
31
+ result[i] = (near * (far / near) ** ((i + 1) / count)) / far;
32
+ }
33
+ result.length = count;
34
+ return result;
35
+ },
36
+
37
+ practical: (count, near, far, lambda = 0.5, result = []) => {
38
+ for (let i = 0; i < count; ++i) {
39
+ const uniform = (near + ((far - near) * (i + 1)) / count) / far;
40
+ const logarithmic = (near * (far / near) ** ((i + 1) / count)) / far;
41
+ result[i] = lerp(uniform, logarithmic, lambda);
42
+ }
43
+ result.length = count;
44
+ return result;
45
+ },
46
+ };
47
+
48
+ export const frustumSplitFunctions = modes;
49
+
50
+ export function splitFrustum(
51
+ mode: FrustumSplitMode,
52
+ count: number,
53
+ near: number,
54
+ far: number,
55
+ lambda?: number,
56
+ result: number[] = [],
57
+ ): number[] {
58
+ return modes[mode](count, near, far, lambda, result);
59
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />