three-realtime-rt 0.3.0 → 0.4.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.
package/package.json CHANGED
@@ -1,52 +1,52 @@
1
- {
2
- "name": "three-realtime-rt",
3
- "version": "0.3.0",
4
- "description": "Turn-on ray traced lighting for three.js: a hybrid deferred renderer with BVH-traced soft shadows, one-bounce global illumination, temporal reprojection, an a-trous denoiser and TAA.",
5
- "type": "module",
6
- "main": "src/index.js",
7
- "module": "src/index.js",
8
- "types": "src/index.d.ts",
9
- "exports": {
10
- ".": "./src/index.js"
11
- },
12
- "files": [
13
- "src"
14
- ],
15
- "sideEffects": false,
16
- "scripts": {
17
- "dev": "vite",
18
- "build": "vite build",
19
- "preview": "vite preview",
20
- "deploy": "vite build && gh-pages -d dist -t"
21
- },
22
- "keywords": [
23
- "three",
24
- "threejs",
25
- "raytracing",
26
- "path-tracing",
27
- "global-illumination",
28
- "webgl",
29
- "bvh",
30
- "denoiser",
31
- "taa",
32
- "renderer"
33
- ],
34
- "homepage": "https://goldwinxs.github.io/three-realtime-rt/",
35
- "repository": {
36
- "type": "git",
37
- "url": "https://github.com/GoldwinXS/three-realtime-rt.git"
38
- },
39
- "author": "Goldwin Stewart",
40
- "license": "MIT",
41
- "peerDependencies": {
42
- "three": ">=0.155.0",
43
- "three-mesh-bvh": ">=0.7.0"
44
- },
45
- "devDependencies": {
46
- "@dimforge/rapier3d-compat": "^0.14.0",
47
- "gh-pages": "^6.3.0",
48
- "three": "^0.160.1",
49
- "three-mesh-bvh": "^0.7.8",
50
- "vite": "^5.0.11"
51
- }
52
- }
1
+ {
2
+ "name": "three-realtime-rt",
3
+ "version": "0.4.0",
4
+ "description": "Turn-on ray traced lighting for three.js: a hybrid deferred renderer with BVH-traced soft shadows, one-bounce global illumination, temporal reprojection, an a-trous denoiser and TAA.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "module": "src/index.js",
8
+ "types": "src/index.d.ts",
9
+ "exports": {
10
+ ".": "./src/index.js"
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "sideEffects": false,
16
+ "scripts": {
17
+ "dev": "vite",
18
+ "build": "vite build",
19
+ "preview": "vite preview",
20
+ "deploy": "vite build && gh-pages -d dist -t"
21
+ },
22
+ "keywords": [
23
+ "three",
24
+ "threejs",
25
+ "raytracing",
26
+ "path-tracing",
27
+ "global-illumination",
28
+ "webgl",
29
+ "bvh",
30
+ "denoiser",
31
+ "taa",
32
+ "renderer"
33
+ ],
34
+ "homepage": "https://goldwinxs.github.io/three-realtime-rt/",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/GoldwinXS/three-realtime-rt.git"
38
+ },
39
+ "author": "Goldwin Stewart",
40
+ "license": "MIT",
41
+ "peerDependencies": {
42
+ "three": ">=0.155.0",
43
+ "three-mesh-bvh": ">=0.7.0"
44
+ },
45
+ "devDependencies": {
46
+ "@dimforge/rapier3d-compat": "^0.14.0",
47
+ "gh-pages": "^6.3.0",
48
+ "three": "^0.160.1",
49
+ "three-mesh-bvh": "^0.7.8",
50
+ "vite": "^5.0.11"
51
+ }
52
+ }
@@ -1,214 +1,261 @@
1
- import * as THREE from "three";
2
- import { SKY_GLSL } from "./sky.glsl.js";
3
-
4
- const fullscreenVert = /* glsl */ `
5
- out vec2 vUv;
6
- void main() {
7
- vUv = uv;
8
- gl_Position = vec4(position.xy, 0.0, 1.0);
9
- }
10
- `;
11
-
12
- const compositeFrag = /* glsl */ `
13
- precision highp float;
14
-
15
- layout(location = 0) out vec4 outColor;
16
-
17
- in vec2 vUv;
18
-
19
- ${SKY_GLSL}
20
-
21
- uniform sampler2D uIrradiance;
22
- uniform sampler2D uGAlbedoRough;
23
- uniform sampler2D uGNormalMetal;
24
- uniform sampler2D uGWorldPos;
25
- uniform sampler2D uGEmissive;
26
- uniform sampler2D uVolumetric; // in-scattered light (lighting res, smooth)
27
- uniform bool uVolEnabled;
28
- uniform vec3 uBackgroundColor;
29
- // 0 composite, 1 albedo, 2 normal, 3 irradiance (direct+GI), 4 worldPos, 5 emissive
30
- uniform int uOutputMode;
31
-
32
- // joint bilateral upsample (lighting may be rendered below full resolution)
33
- uniform bool uUpsample;
34
- uniform vec2 uIrrTexelSize;
35
- uniform vec3 uCameraPos;
36
-
37
- // distance fog (applied in linear space, before tonemap)
38
- uniform bool uFogEnabled;
39
- uniform vec3 uFogColor;
40
- uniform float uFogDensity;
41
-
42
- // procedural sky background
43
- uniform bool uSkyEnabled;
44
- uniform mat4 uInvViewProj;
45
- uniform vec3 uSunDir;
46
- uniform vec3 uSunColor;
47
- uniform vec3 uSkyZenith;
48
- uniform vec3 uSkyHorizon;
49
- uniform float uSkyIntensity;
50
-
51
- // Reconstruct the world-space view ray for this pixel from the inverse VP.
52
- vec3 viewRay(vec2 uv) {
53
- vec4 far = uInvViewProj * vec4(uv * 2.0 - 1.0, 1.0, 1.0);
54
- return normalize(far.xyz / far.w - uCameraPos);
55
- }
56
-
57
- vec3 acesFilm(vec3 x) {
58
- const float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
59
- return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
60
- }
61
-
62
- // Upsample low-res irradiance to this full-res pixel: 4 nearest low-res taps,
63
- // bilinear weights modulated by geometric similarity (plane distance + normal)
64
- // so lighting never bleeds across depth or orientation discontinuities.
65
- vec3 sampleIrradiance(vec2 uv, vec3 P, vec3 N) {
66
- if (!uUpsample) return texture(uIrradiance, uv).rgb;
67
-
68
- float planeTol = 0.01 * distance(P, uCameraPos) + 1e-3;
69
- vec2 base = uv / uIrrTexelSize - 0.5;
70
- vec2 f = fract(base);
71
- vec2 uv00 = (floor(base) + 0.5) * uIrrTexelSize;
72
-
73
- vec3 sum = vec3(0.0);
74
- float wsum = 0.0;
75
- vec3 bestGeo = vec3(0.0);
76
- float bestGeoW = -1.0;
77
- vec3 bestBil = vec3(0.0);
78
- float bestBilW = -1.0;
79
- for (int dy = 0; dy <= 1; dy++) {
80
- for (int dx = 0; dx <= 1; dx++) {
81
- vec2 tuv = uv00 + vec2(float(dx), float(dy)) * uIrrTexelSize;
82
- vec3 irr = texture(uIrradiance, tuv).rgb;
83
- float bw = (dx == 0 ? 1.0 - f.x : f.x) * (dy == 0 ? 1.0 - f.y : f.y);
84
- if (bw > bestBilW) { bestBilW = bw; bestBil = irr; }
85
-
86
- vec4 g = texture(uGWorldPos, tuv);
87
- if (g.w < 0.5) continue;
88
- vec3 Nt = normalize(texture(uGNormalMetal, tuv).xyz);
89
- float wPlane = exp(-abs(dot(g.xyz - P, N)) / planeTol);
90
- float wN = pow(max(dot(N, Nt), 0.0), 16.0);
91
- float gw = wPlane * wN;
92
- // Track the geometrically most similar tap for the fallback below.
93
- if (gw > bestGeoW) { bestGeoW = gw; bestGeo = irr; }
94
- float w = bw * gw;
95
- sum += irr * w;
96
- wsum += w;
97
- }
98
- }
99
- if (wsum > 1e-4) return sum / wsum;
100
- // All combined weights died (thin silhouette). Falling back to the closest
101
- // *bilinear* tap would pull lighting from the far side of the edge — under
102
- // TAA jitter the chosen tap flickers, which reads as bright "rain drop"
103
- // speckles on dark objects. Prefer the geometrically closest tap instead.
104
- return bestGeoW >= 0.0 ? bestGeo : bestBil;
105
- }
106
-
107
- void main() {
108
- vec4 wp = texture(uGWorldPos, vUv);
109
- vec4 albedoRough = texture(uGAlbedoRough, vUv);
110
- vec3 N = normalize(texture(uGNormalMetal, vUv).xyz);
111
- vec3 irradiance = sampleIrradiance(vUv, wp.xyz, N);
112
- vec3 emissive = texture(uGEmissive, vUv).rgb;
113
-
114
- vec3 color;
115
- if (wp.w < 0.5) {
116
- // Background: the procedural sky (with sun), else fog colour, else flat.
117
- if (uSkyEnabled) {
118
- color = skyColor(viewRay(vUv), uSunDir, uSunColor, uSkyZenith, uSkyHorizon, uSkyIntensity);
119
- } else {
120
- color = uFogEnabled ? uFogColor : uBackgroundColor;
121
- }
122
- } else {
123
- color = albedoRough.rgb * irradiance + emissive;
124
- // Volumetric in-scatter (already radiance, not modulated by albedo). Fog
125
- // is low-frequency, so a wide 5-tap blur costs nothing visually and eats
126
- // most of the single-sample grain the accumulation hasn't averaged yet.
127
- if (uVolEnabled) {
128
- vec2 o = uIrrTexelSize * 1.5;
129
- vec3 vol = texture(uVolumetric, vUv).rgb * 0.4
130
- + texture(uVolumetric, vUv + vec2( o.x, o.y)).rgb * 0.15
131
- + texture(uVolumetric, vUv + vec2(-o.x, o.y)).rgb * 0.15
132
- + texture(uVolumetric, vUv + vec2( o.x, -o.y)).rgb * 0.15
133
- + texture(uVolumetric, vUv + vec2(-o.x, -o.y)).rgb * 0.15;
134
- color += vol;
135
- }
136
- if (uFogEnabled) {
137
- float dist = distance(wp.xyz, uCameraPos);
138
- float f = 1.0 - exp(-uFogDensity * uFogDensity * dist * dist);
139
- color = mix(color, uFogColor, clamp(f, 0.0, 1.0));
140
- }
141
- }
142
-
143
- if (uOutputMode == 1) color = albedoRough.rgb;
144
- else if (uOutputMode == 2) color = N * 0.5 + 0.5;
145
- else if (uOutputMode == 3) color = irradiance;
146
- else if (uOutputMode == 4) color = fract(wp.xyz * 0.1);
147
- else if (uOutputMode == 5) color = emissive;
148
- else color = acesFilm(color);
149
-
150
- outColor = vec4(pow(color, vec3(1.0 / 2.2)), 1.0);
151
- }
152
- `;
153
-
154
- /** Combines G-buffer albedo/emissive with accumulated irradiance, tonemaps, writes to screen. */
155
- export class CompositePass {
156
- constructor() {
157
- this.material = new THREE.ShaderMaterial({
158
- glslVersion: THREE.GLSL3,
159
- vertexShader: fullscreenVert,
160
- fragmentShader: compositeFrag,
161
- uniforms: {
162
- uIrradiance: { value: null },
163
- uGAlbedoRough: { value: null },
164
- uGNormalMetal: { value: null },
165
- uGWorldPos: { value: null },
166
- uGEmissive: { value: null },
167
- uVolumetric: { value: null },
168
- uVolEnabled: { value: false },
169
- uBackgroundColor: { value: new THREE.Color(0.01, 0.012, 0.02) },
170
- uOutputMode: { value: 0 },
171
- uUpsample: { value: false },
172
- uIrrTexelSize: { value: new THREE.Vector2() },
173
- uCameraPos: { value: new THREE.Vector3() },
174
- uFogEnabled: { value: false },
175
- uFogColor: { value: new THREE.Color(0.5, 0.6, 0.7) },
176
- uFogDensity: { value: 0.05 },
177
- uSkyEnabled: { value: false },
178
- uInvViewProj: { value: new THREE.Matrix4() },
179
- uSunDir: { value: new THREE.Vector3(0.4, 0.8, 0.45).normalize() },
180
- uSunColor: { value: new THREE.Color(1.0, 0.9, 0.75) },
181
- uSkyZenith: { value: new THREE.Color(0.18, 0.34, 0.62) },
182
- uSkyHorizon: { value: new THREE.Color(0.7, 0.8, 0.9) },
183
- uSkyIntensity: { value: 1.0 },
184
- },
185
- depthTest: false,
186
- depthWrite: false,
187
- });
188
-
189
- this.scene = new THREE.Scene();
190
- this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
191
- this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
192
- this.quad.frustumCulled = false;
193
- this.scene.add(this.quad);
194
- }
195
-
196
- render(renderer, irradianceTexture, gbuffer, background, target = null) {
197
- const u = this.material.uniforms;
198
- u.uIrradiance.value = irradianceTexture;
199
- u.uGAlbedoRough.value = gbuffer.albedoRough;
200
- u.uGNormalMetal.value = gbuffer.normalMetal;
201
- u.uGWorldPos.value = gbuffer.worldPos;
202
- u.uGEmissive.value = gbuffer.emissive;
203
- if (background && background.isColor) {
204
- u.uBackgroundColor.value.copy(background);
205
- }
206
- renderer.setRenderTarget(target);
207
- renderer.render(this.scene, this.camera);
208
- }
209
-
210
- dispose() {
211
- this.material.dispose();
212
- this.quad.geometry.dispose();
213
- }
214
- }
1
+ import * as THREE from "three";
2
+ import { SKY_GLSL } from "./sky.glsl.js";
3
+
4
+ const fullscreenVert = /* glsl */ `
5
+ out vec2 vUv;
6
+ void main() {
7
+ vUv = uv;
8
+ gl_Position = vec4(position.xy, 0.0, 1.0);
9
+ }
10
+ `;
11
+
12
+ const compositeFrag = /* glsl */ `
13
+ precision highp float;
14
+
15
+ layout(location = 0) out vec4 outColor;
16
+
17
+ in vec2 vUv;
18
+
19
+ ${SKY_GLSL}
20
+
21
+ uniform sampler2D uIrradiance;
22
+ uniform sampler2D uSpecular; // dielectric direct specular (added WITHOUT albedo)
23
+ uniform bool uSpecEnabled;
24
+ uniform sampler2D uGAlbedoRough;
25
+ uniform sampler2D uGNormalMetal;
26
+ uniform sampler2D uGWorldPos;
27
+ uniform sampler2D uGEmissive;
28
+ uniform sampler2D uVolumetric; // in-scattered light (quarter canvas res, smooth)
29
+ uniform vec2 uVolTexelSize;
30
+ uniform bool uVolEnabled;
31
+ uniform vec3 uBackgroundColor;
32
+ // 0 composite, 1 albedo, 2 normal, 3 irradiance (direct+GI), 4 worldPos,
33
+ // 5 emissive, 6 specular
34
+ uniform int uOutputMode;
35
+
36
+ // joint bilateral upsample (lighting may be rendered below full resolution)
37
+ uniform bool uUpsample;
38
+ uniform vec2 uIrrTexelSize;
39
+ uniform vec3 uCameraPos;
40
+
41
+ // Overscan crop: maps this on-screen pixel's UV into the central region of the
42
+ // padded internal image (scale.xy, offset.zw). Identity (1,1,0,0) when overscan
43
+ // is 0 or when compositing into the offscreen target that TAA later crops.
44
+ uniform vec4 uCrop;
45
+
46
+ // distance fog (applied in linear space, before tonemap)
47
+ uniform bool uFogEnabled;
48
+ uniform vec3 uFogColor;
49
+ uniform float uFogDensity;
50
+
51
+ // procedural sky background
52
+ uniform bool uSkyEnabled;
53
+ uniform mat4 uInvViewProj;
54
+ uniform vec3 uSunDir;
55
+ uniform vec3 uSunColor;
56
+ uniform vec3 uSkyZenith;
57
+ uniform vec3 uSkyHorizon;
58
+ uniform float uSkyIntensity;
59
+
60
+ // Reconstruct the world-space view ray for this pixel from the inverse VP.
61
+ vec3 viewRay(vec2 uv) {
62
+ vec4 far = uInvViewProj * vec4(uv * 2.0 - 1.0, 1.0, 1.0);
63
+ return normalize(far.xyz / far.w - uCameraPos);
64
+ }
65
+
66
+ vec3 acesFilm(vec3 x) {
67
+ const float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
68
+ return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
69
+ }
70
+
71
+ // Upsample low-res irradiance to this full-res pixel: 4 nearest low-res taps,
72
+ // bilinear weights modulated by geometric similarity (plane distance + normal)
73
+ // so lighting never bleeds across depth or orientation discontinuities.
74
+ vec3 upsampleGuided(sampler2D tex, vec2 uv, vec3 P, vec3 N) {
75
+ if (!uUpsample) return texture(tex, uv).rgb;
76
+
77
+ float planeTol = 0.01 * distance(P, uCameraPos) + 1e-3;
78
+ vec2 base = uv / uIrrTexelSize - 0.5;
79
+ vec2 f = fract(base);
80
+ vec2 uv00 = (floor(base) + 0.5) * uIrrTexelSize;
81
+
82
+ vec3 sum = vec3(0.0);
83
+ float wsum = 0.0;
84
+ vec3 bestGeo = vec3(0.0);
85
+ float bestGeoW = -1.0;
86
+ vec3 bestBil = vec3(0.0);
87
+ float bestBilW = -1.0;
88
+ for (int dy = 0; dy <= 1; dy++) {
89
+ for (int dx = 0; dx <= 1; dx++) {
90
+ vec2 tuv = uv00 + vec2(float(dx), float(dy)) * uIrrTexelSize;
91
+ vec3 irr = texture(tex, tuv).rgb;
92
+ float bw = (dx == 0 ? 1.0 - f.x : f.x) * (dy == 0 ? 1.0 - f.y : f.y);
93
+ if (bw > bestBilW) { bestBilW = bw; bestBil = irr; }
94
+
95
+ vec4 g = texture(uGWorldPos, tuv);
96
+ if (g.w < 0.5) continue;
97
+ vec3 Nt = normalize(texture(uGNormalMetal, tuv).xyz);
98
+ float wPlane = exp(-abs(dot(g.xyz - P, N)) / planeTol);
99
+ float wN = pow(max(dot(N, Nt), 0.0), 16.0);
100
+ float gw = wPlane * wN;
101
+ // Track the geometrically most similar tap for the fallback below.
102
+ if (gw > bestGeoW) { bestGeoW = gw; bestGeo = irr; }
103
+ float w = bw * gw;
104
+ sum += irr * w;
105
+ wsum += w;
106
+ }
107
+ }
108
+ if (wsum > 1e-4) return sum / wsum;
109
+ // All combined weights died (thin silhouette). Falling back to the closest
110
+ // *bilinear* tap would pull lighting from the far side of the edge — under
111
+ // TAA jitter the chosen tap flickers, which reads as bright "rain drop"
112
+ // speckles on dark objects. Prefer the geometrically closest tap instead.
113
+ return bestGeoW >= 0.0 ? bestGeo : bestBil;
114
+ }
115
+
116
+ void main() {
117
+ // Sample the padded internal image at the cropped UV (identity when no crop).
118
+ // Everything below lives in padded space, so one remap here covers all taps.
119
+ vec2 uv = vUv * uCrop.xy + uCrop.zw;
120
+ vec4 wp = texture(uGWorldPos, uv);
121
+ vec4 albedoRough = texture(uGAlbedoRough, uv);
122
+ vec4 nmFull = texture(uGNormalMetal, uv);
123
+ vec3 N = normalize(nmFull.xyz);
124
+ vec3 irradiance = upsampleGuided(uIrradiance, uv, wp.xyz, N);
125
+ vec3 specular = uSpecEnabled ? upsampleGuided(uSpecular, uv, wp.xyz, N) : vec3(0.0);
126
+ vec3 emissive = texture(uGEmissive, uv).rgb;
127
+
128
+ vec3 color;
129
+ if (wp.w < 0.5) {
130
+ // Background: the procedural sky (with sun), else fog colour, else flat.
131
+ if (uSkyEnabled) {
132
+ color = skyColor(viewRay(uv), uSunDir, uSunColor, uSkyZenith, uSkyHorizon, uSkyIntensity);
133
+ } else {
134
+ color = uFogEnabled ? uFogColor : uBackgroundColor;
135
+ }
136
+ } else {
137
+ // Diffuse is demodulated (albedo re-applied here); the dielectric specular
138
+ // highlight is white (F0 ~= 0.04) and is added WITHOUT the albedo multiply.
139
+ if (nmFull.w >= 4.0) {
140
+ // Alpha blend (packed word >= 4, opacity = w - 4): the irradiance slot
141
+ // holds the pane's own demodulated surface light and the SPECULAR slot
142
+ // carries the traced radiance from BEHIND the pane (see RTLightingPass) —
143
+ // the only place both quantities exist at final-pixel scale alongside the
144
+ // pane's albedo, so the opacity blend happens here. With the specular
145
+ // buffer disabled there is no behind-image; degrade to an opaque pane.
146
+ float opacity = clamp(nmFull.w - 4.0, 0.0, 1.0);
147
+ vec3 paneCol = albedoRough.rgb * irradiance + emissive;
148
+ color = uSpecEnabled ? mix(specular, paneCol, opacity) : paneCol;
149
+ } else {
150
+ color = albedoRough.rgb * irradiance + specular + emissive;
151
+ }
152
+ // Volumetric in-scatter (already radiance, not modulated by albedo). Fog
153
+ // is low-frequency, so a wide 9-tap blur costs nothing visually and eats
154
+ // the single-sample grain — crucial with MOVING lights, where the
155
+ // in-scatter field changes every frame and temporal accumulation alone
156
+ // can never converge it (grain carpeted dark scenes otherwise).
157
+ if (uVolEnabled) {
158
+ vec2 o1 = uVolTexelSize * 1.5;
159
+ vec2 o2 = uVolTexelSize * 3.5;
160
+ vec3 vol = texture(uVolumetric, uv).rgb * 0.24
161
+ + texture(uVolumetric, uv + vec2( o1.x, o1.y)).rgb * 0.12
162
+ + texture(uVolumetric, uv + vec2(-o1.x, o1.y)).rgb * 0.12
163
+ + texture(uVolumetric, uv + vec2( o1.x, -o1.y)).rgb * 0.12
164
+ + texture(uVolumetric, uv + vec2(-o1.x, -o1.y)).rgb * 0.12
165
+ + texture(uVolumetric, uv + vec2( o2.x, 0.0 )).rgb * 0.07
166
+ + texture(uVolumetric, uv + vec2(-o2.x, 0.0 )).rgb * 0.07
167
+ + texture(uVolumetric, uv + vec2( 0.0 , o2.y)).rgb * 0.07
168
+ + texture(uVolumetric, uv + vec2( 0.0 , -o2.y)).rgb * 0.07;
169
+ color += vol;
170
+ }
171
+ if (uFogEnabled) {
172
+ float dist = distance(wp.xyz, uCameraPos);
173
+ float f = 1.0 - exp(-uFogDensity * uFogDensity * dist * dist);
174
+ color = mix(color, uFogColor, clamp(f, 0.0, 1.0));
175
+ }
176
+ }
177
+
178
+ if (uOutputMode == 1) color = albedoRough.rgb;
179
+ else if (uOutputMode == 2) color = N * 0.5 + 0.5;
180
+ else if (uOutputMode == 3) color = irradiance;
181
+ else if (uOutputMode == 4) color = fract(wp.xyz * 0.1);
182
+ else if (uOutputMode == 5) color = emissive;
183
+ else if (uOutputMode == 6) color = specular;
184
+ else color = acesFilm(color);
185
+
186
+ outColor = vec4(pow(color, vec3(1.0 / 2.2)), 1.0);
187
+ }
188
+ `;
189
+
190
+ /** Combines G-buffer albedo/emissive with accumulated irradiance, tonemaps, writes to screen. */
191
+ export class CompositePass {
192
+ constructor() {
193
+ this.material = new THREE.ShaderMaterial({
194
+ glslVersion: THREE.GLSL3,
195
+ vertexShader: fullscreenVert,
196
+ fragmentShader: compositeFrag,
197
+ uniforms: {
198
+ uIrradiance: { value: null },
199
+ uSpecular: { value: null },
200
+ uSpecEnabled: { value: false },
201
+ uGAlbedoRough: { value: null },
202
+ uGNormalMetal: { value: null },
203
+ uGWorldPos: { value: null },
204
+ uGEmissive: { value: null },
205
+ uVolumetric: { value: null },
206
+ uVolTexelSize: { value: new THREE.Vector2() },
207
+ uVolEnabled: { value: false },
208
+ uBackgroundColor: { value: new THREE.Color(0.01, 0.012, 0.02) },
209
+ uOutputMode: { value: 0 },
210
+ uUpsample: { value: false },
211
+ uIrrTexelSize: { value: new THREE.Vector2() },
212
+ uCameraPos: { value: new THREE.Vector3() },
213
+ uCrop: { value: new THREE.Vector4(1, 1, 0, 0) },
214
+ uFogEnabled: { value: false },
215
+ uFogColor: { value: new THREE.Color(0.5, 0.6, 0.7) },
216
+ uFogDensity: { value: 0.05 },
217
+ uSkyEnabled: { value: false },
218
+ uInvViewProj: { value: new THREE.Matrix4() },
219
+ uSunDir: { value: new THREE.Vector3(0.4, 0.8, 0.45).normalize() },
220
+ uSunColor: { value: new THREE.Color(1.0, 0.9, 0.75) },
221
+ uSkyZenith: { value: new THREE.Color(0.18, 0.34, 0.62) },
222
+ uSkyHorizon: { value: new THREE.Color(0.7, 0.8, 0.9) },
223
+ uSkyIntensity: { value: 1.0 },
224
+ },
225
+ depthTest: false,
226
+ depthWrite: false,
227
+ });
228
+
229
+ this.scene = new THREE.Scene();
230
+ this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
231
+ this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
232
+ this.quad.frustumCulled = false;
233
+ this.scene.add(this.quad);
234
+ }
235
+
236
+ // `crop` is the overscan central-crop transform (THREE.Vector4 scale.xy /
237
+ // offset.zw); pass null for no crop (identity) — used when compositing into the
238
+ // padded offscreen target that TAA crops later.
239
+ render(renderer, irradianceTexture, gbuffer, background, target = null, specularTexture = null, crop = null) {
240
+ const u = this.material.uniforms;
241
+ u.uIrradiance.value = irradianceTexture;
242
+ u.uSpecular.value = specularTexture;
243
+ u.uSpecEnabled.value = specularTexture !== null;
244
+ u.uGAlbedoRough.value = gbuffer.albedoRough;
245
+ u.uGNormalMetal.value = gbuffer.normalMetal;
246
+ u.uGWorldPos.value = gbuffer.worldPos;
247
+ u.uGEmissive.value = gbuffer.emissive;
248
+ if (crop) u.uCrop.value.copy(crop);
249
+ else u.uCrop.value.set(1, 1, 0, 0);
250
+ if (background && background.isColor) {
251
+ u.uBackgroundColor.value.copy(background);
252
+ }
253
+ renderer.setRenderTarget(target);
254
+ renderer.render(this.scene, this.camera);
255
+ }
256
+
257
+ dispose() {
258
+ this.material.dispose();
259
+ this.quad.geometry.dispose();
260
+ }
261
+ }