three-realtime-rt 0.3.2 → 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.
@@ -37,6 +37,32 @@ uniform sampler2D uMap;
37
37
  uniform bool uHasMap;
38
38
  uniform sampler2D uEmissiveMap;
39
39
  uniform bool uHasEmissiveMap;
40
+ // PBR texture maps (raster pass has ample sampler headroom, unlike the lighting
41
+ // pass). All guarded by a uHas* flag so a material without a given map writes
42
+ // exactly the same bytes it did before these were added.
43
+ uniform sampler2D uNormalMap;
44
+ uniform bool uHasNormalMap;
45
+ uniform vec2 uNormalScale;
46
+ uniform sampler2D uRoughnessMap;
47
+ uniform bool uHasRoughnessMap;
48
+ uniform sampler2D uMetalnessMap;
49
+ uniform bool uHasMetalnessMap;
50
+ uniform bool uBlend;
51
+ uniform float uOpacity;
52
+
53
+ // Screen-space cotangent frame (Mikkelsen 2010): reconstruct a tangent basis
54
+ // from derivatives of world position and uv, so tangent-space normal maps work
55
+ // without a per-vertex tangent attribute (none is uploaded to the BVH/G-buffer).
56
+ vec3 perturbNormal(vec3 N, vec3 P, vec2 uv, vec3 mapN) {
57
+ vec3 dpdx = dFdx(P);
58
+ vec3 dpdy = dFdy(P);
59
+ vec2 duvdx = dFdx(uv);
60
+ vec2 duvdy = dFdy(uv);
61
+ vec3 t = normalize(dpdx * duvdy.y - dpdy * duvdx.y);
62
+ vec3 b = normalize(cross(N, t));
63
+ mat3 tbn = mat3(t, b, N);
64
+ return normalize(tbn * mapN);
65
+ }
40
66
 
41
67
  void main() {
42
68
  vec3 albedo = uColor;
@@ -48,16 +74,37 @@ void main() {
48
74
  emissive *= texture(uEmissiveMap, vUvCoord).rgb;
49
75
  }
50
76
  vec3 n = normalize(vWorldNormal) * (gl_FrontFacing ? 1.0 : -1.0);
51
- gAlbedoRough = vec4(albedo, uRoughness);
52
- // .w is a packed material word: >= 2 means transmissive glass (w - 2 =
53
- // transmission amount), else it is plain metalness. Lets the lighting pass
54
- // read specular/glass properties without an extra G-buffer sampler (it
55
- // already sits at the WebGL2 16-sampler minimum).
56
- gNormalMetal = vec4(n, uTransmission > 0.0 ? 2.0 + uTransmission : uMetalness);
77
+ if (uHasNormalMap) {
78
+ // Tangent-space normal in [-1,1], scaled by material.normalScale (x,y).
79
+ vec3 mapN = texture(uNormalMap, vUvCoord).xyz * 2.0 - 1.0;
80
+ mapN.xy *= uNormalScale;
81
+ n = perturbNormal(n, vWorldPos, vUvCoord, mapN);
82
+ }
83
+ // three.js convention: green channel of roughnessMap x scalar roughness,
84
+ // blue channel of metalnessMap x scalar metalness (an ORM texture packs both).
85
+ float roughness = uRoughness;
86
+ if (uHasRoughnessMap) roughness *= texture(uRoughnessMap, vUvCoord).g;
87
+ float metalness = uMetalness;
88
+ if (uHasMetalnessMap) metalness *= texture(uMetalnessMap, vUvCoord).b;
89
+ gAlbedoRough = vec4(albedo, roughness);
90
+ // .w is a packed material word in three disjoint ranges, so the lighting pass
91
+ // reads specular/glass/blend properties without an extra G-buffer sampler (it
92
+ // already sits at the WebGL2 16-sampler minimum):
93
+ // [0,1] plain metalness [2,3] transmissive glass (w - 2 = transmission)
94
+ // [4,5] alpha blend (w - 4 = opacity). Blend wins: a transparent surface is
95
+ // kept out of the BVH and composited by the lighting pass, so it must never be
96
+ // read as glass even if the material also carries a transmission value.
97
+ float matWord = uBlend
98
+ ? 4.0 + uOpacity
99
+ : (uTransmission > 0.0 ? 2.0 + uTransmission : metalness);
100
+ gNormalMetal = vec4(n, matWord);
57
101
  // .w packs the valid flag AND roughness: 0 = background, 1 + roughness
58
102
  // otherwise. Every consumer only tests w < 0.5, so this stays compatible.
59
- gWorldPos = vec4(vWorldPos, 1.0 + uRoughness);
60
- gEmissive = vec4(emissive, 1.0);
103
+ gWorldPos = vec4(vWorldPos, 1.0 + roughness);
104
+ // .a is normally the constant 1.0 (CompositePass reads only .rgb). A blend
105
+ // surface carries its opacity here; the packed word above also encodes it, so
106
+ // the sampler-bound lighting pass reads opacity without a gEmissive fetch.
107
+ gEmissive = vec4(emissive, uBlend ? uOpacity : 1.0);
61
108
  }
62
109
  `;
63
110
 
@@ -155,6 +202,15 @@ export class GBufferPass {
155
202
  uHasMap: { value: false },
156
203
  uEmissiveMap: { value: null },
157
204
  uHasEmissiveMap: { value: false },
205
+ uNormalMap: { value: null },
206
+ uHasNormalMap: { value: false },
207
+ uNormalScale: { value: new THREE.Vector2(1, 1) },
208
+ uRoughnessMap: { value: null },
209
+ uHasRoughnessMap: { value: false },
210
+ uMetalnessMap: { value: null },
211
+ uHasMetalnessMap: { value: false },
212
+ uBlend: { value: false },
213
+ uOpacity: { value: 1.0 },
158
214
  },
159
215
  side: THREE.FrontSide,
160
216
  });
@@ -178,6 +234,19 @@ export class GBufferPass {
178
234
  u.uHasMap.value = !!src.map;
179
235
  u.uEmissiveMap.value = src.emissiveMap ?? null;
180
236
  u.uHasEmissiveMap.value = !!src.emissiveMap;
237
+ u.uNormalMap.value = src.normalMap ?? null;
238
+ u.uHasNormalMap.value = !!src.normalMap;
239
+ if (src.normalScale) u.uNormalScale.value.copy(src.normalScale);
240
+ else u.uNormalScale.value.set(1, 1);
241
+ u.uRoughnessMap.value = src.roughnessMap ?? null;
242
+ u.uHasRoughnessMap.value = !!src.roughnessMap;
243
+ u.uMetalnessMap.value = src.metalnessMap ?? null;
244
+ u.uHasMetalnessMap.value = !!src.metalnessMap;
245
+ // Alpha-blended transparency: primary-visible here (opacity packed into the
246
+ // material word + gEmissive.a), composited against the geometry behind by the
247
+ // lighting pass. opacity 1 renders opaque, matching the old force-opaque path.
248
+ u.uBlend.value = !!src.transparent;
249
+ u.uOpacity.value = src.opacity ?? 1.0;
181
250
  u.uNormalMatrixWorld.value.getNormalMatrix(mesh.matrixWorld);
182
251
  material.side = src.side ?? THREE.FrontSide;
183
252
  return material;
@@ -187,19 +256,15 @@ export class GBufferPass {
187
256
  // Ping-pong: what was "current" becomes "previous".
188
257
  this._current = 1 - this._current;
189
258
 
190
- // Swap in G-buffer materials.
259
+ // Swap in G-buffer materials. Transparent meshes are written too — as the
260
+ // nearest single layer, depth-tested/written like any surface (overlapping
261
+ // transparent surfaces do not inter-sort). Their opacity rides in the packed
262
+ // material word + gEmissive.a, and the lighting pass blends them against the
263
+ // geometry behind. opacity 1 writes fully opaque, so alpha-textured cases
264
+ // (LittlestTokyo's glass) look exactly as before.
191
265
  this._swapped.length = 0;
192
266
  scene.traverse((obj) => {
193
267
  if (obj.isMesh && obj.geometry && obj.visible) {
194
- // The G-buffer has no alpha blending — a nearly invisible surface
195
- // (glass display case, ghost meshes) would write itself opaque over
196
- // everything behind it. Hide it from the raster pass instead.
197
- const m = Array.isArray(obj.material) ? obj.material[0] : obj.material;
198
- if (m.transparent && (m.opacity ?? 1) < 0.5) {
199
- obj.visible = false;
200
- this._swapped.push([obj, null]); // restore visibility after render
201
- return;
202
- }
203
268
  this._swapped.push([obj, obj.material]);
204
269
  obj.material = this._gbufferMaterialFor(obj);
205
270
  }
@@ -215,10 +280,7 @@ export class GBufferPass {
215
280
  renderer.setRenderTarget(null);
216
281
 
217
282
  scene.background = prevBackground;
218
- for (const [mesh, mat] of this._swapped) {
219
- if (mat === null) mesh.visible = true; // was hidden, not material-swapped
220
- else mesh.material = mat;
221
- }
283
+ for (const [mesh, mat] of this._swapped) mesh.material = mat;
222
284
  this._swapped.length = 0;
223
285
  }
224
286