rayzee 7.22.8 → 7.23.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/dist/rayzee.es.js +1541 -1499
- package/dist/rayzee.umd.js +54 -52
- package/package.json +1 -1
- package/src/EngineDefaults.js +5 -0
- package/src/PathTracerApp.js +12 -0
- package/src/Processor/GeometryExtractor.js +5 -13
- package/src/Processor/TextureCreator.js +2 -2
- package/src/Stages/NormalDepth.js +11 -2
- package/src/TSL/Debugger.js +5 -5
- package/src/TSL/MaterialTransmission.js +4 -45
- package/src/TSL/ShadeKernel.js +11 -2
- package/src/TSL/TextureSampling.js +97 -8
- package/src/managers/MaterialDataManager.js +51 -3
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -524,6 +524,11 @@ export const MATERIAL_DATA_LAYOUT = {
|
|
|
524
524
|
|
|
525
525
|
};
|
|
526
526
|
|
|
527
|
+
// glTF/three.js spell "no volume absorption" as attenuationDistance = Infinity, while the GPU
|
|
528
|
+
// contract is `> 0 = on` (calculateBeerLawAbsorption). Every writer into ATTENUATION_DISTANCE
|
|
529
|
+
// collapses both spellings to 0 so no shader divides by Inf.
|
|
530
|
+
export const normalizeAttenuationDistance = d => ( Number.isFinite( d ) && d > 0 ? d : 0 );
|
|
531
|
+
|
|
527
532
|
// BVH node leaf markers
|
|
528
533
|
export const BVH_LEAF_MARKERS = {
|
|
529
534
|
TRIANGLE_LEAF: - 1, // Leaf containing triangle references
|
package/src/PathTracerApp.js
CHANGED
|
@@ -2104,6 +2104,18 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2104
2104
|
|
|
2105
2105
|
}
|
|
2106
2106
|
|
|
2107
|
+
/**
|
|
2108
|
+
* Read back the scalar material property the shader is actually using.
|
|
2109
|
+
* @param {number} materialIndex
|
|
2110
|
+
* @param {string} property
|
|
2111
|
+
* @returns {number|undefined}
|
|
2112
|
+
*/
|
|
2113
|
+
getMaterialProperty( materialIndex, property ) {
|
|
2114
|
+
|
|
2115
|
+
return this.stages.pathTracer?.materialData.getMaterialProperty( materialIndex, property );
|
|
2116
|
+
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2107
2119
|
setMaterialProperty( materialIndex, property, value ) {
|
|
2108
2120
|
|
|
2109
2121
|
this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
|
|
@@ -373,20 +373,12 @@ export class GeometryExtractor {
|
|
|
373
373
|
const materialType = this.getMaterialType( material );
|
|
374
374
|
const legacyMapping = this.mapLegacyMaterialToPhysical( material, materialType );
|
|
375
375
|
|
|
376
|
-
//
|
|
376
|
+
// A transmissive surface is a dielectric interface whose IOR drives refraction, so it wins
|
|
377
|
+
// over the metal Fresnel hack: glTF defaults metallicFactor to 1, which otherwise stamps
|
|
378
|
+
// 2.5 onto water/glass authored as MeshStandardMaterial.
|
|
379
|
+
const isTransmissive = ( material.transmission ?? 0.0 ) > 0.0;
|
|
377
380
|
const isMetallic = ( material.metalness ?? legacyMapping.metalness ?? 0.0 ) > 0.1;
|
|
378
|
-
|
|
379
|
-
// Set appropriate IOR based on material type
|
|
380
|
-
let defaultIOR = defaults.ior;
|
|
381
|
-
if ( isMetallic ) {
|
|
382
|
-
|
|
383
|
-
defaultIOR = 2.5; // Typical metallic IOR
|
|
384
|
-
|
|
385
|
-
} else if ( material.transmission > 0.0 ) {
|
|
386
|
-
|
|
387
|
-
defaultIOR = 1.5; // Glass-like for transmissive materials
|
|
388
|
-
|
|
389
|
-
}
|
|
381
|
+
const defaultIOR = ( isMetallic && ! isTransmissive ) ? 2.5 : defaults.ior;
|
|
390
382
|
|
|
391
383
|
// Handle color conversion for different material types
|
|
392
384
|
let baseColor = material.color || new Color( 0xffffff );
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, RepeatWrapping } from "three";
|
|
2
|
-
import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT } from '../EngineDefaults.js';
|
|
2
|
+
import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT, normalizeAttenuationDistance } from '../EngineDefaults.js';
|
|
3
3
|
import TexturesWorker from './Workers/TexturesWorker.js?worker&inline';
|
|
4
4
|
|
|
5
5
|
// Canvas pooling for efficient reuse of canvas elements
|
|
@@ -948,7 +948,7 @@ export class TextureCreator {
|
|
|
948
948
|
// Slot 0: shadow core (ior, transmission, thickness, emissiveIntensity)
|
|
949
949
|
mat.ior, mat.transmission, mat.thickness, mat.emissiveIntensity,
|
|
950
950
|
// Slot 1: shadow (attenuationColor, attenuationDistance)
|
|
951
|
-
mat.attenuationColor.r, mat.attenuationColor.g, mat.attenuationColor.b, mat.attenuationDistance,
|
|
951
|
+
mat.attenuationColor.r, mat.attenuationColor.g, mat.attenuationColor.b, normalizeAttenuationDistance( mat.attenuationDistance ),
|
|
952
952
|
// Slot 2: shadow + culling (opacity, side, transparent, alphaTest)
|
|
953
953
|
mat.opacity, mat.side, mat.transparent, mat.alphaTest,
|
|
954
954
|
// Slot 3: shadow (alphaMode, depthWrite, normalScale)
|
|
@@ -8,7 +8,7 @@ import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
|
|
|
8
8
|
import { traverseBVH } from '../TSL/BVHTraversal.js';
|
|
9
9
|
import { cameraRayDirection } from '../TSL/CameraRay.js';
|
|
10
10
|
import { getMaterial } from '../TSL/Common.js';
|
|
11
|
-
import { computeUVCache, processNormal, processBump, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
|
|
11
|
+
import { computeUVCache, processNormal, processBump, triangleUVTangent, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* NormalDepth — primary-ray G-buffer for SVGF gates.
|
|
@@ -263,7 +263,16 @@ export class NormalDepth extends RenderStage {
|
|
|
263
263
|
getMaterial( hit.materialIndex, matStorage )
|
|
264
264
|
).toVar();
|
|
265
265
|
const uvCache = UVCache.wrap( computeUVCache( hit.uv, material ) ).toVar();
|
|
266
|
-
|
|
266
|
+
// Same UV tangent frame the Shade kernel uses, or the aux normal disagrees
|
|
267
|
+
// with the shading normal it is supposed to guide the denoiser with.
|
|
268
|
+
const uvTangent = vec4( 0.0 ).toVar();
|
|
269
|
+
If( material.normalMapIndex.greaterThanEqual( int( 0 ) ), () => {
|
|
270
|
+
|
|
271
|
+
uvTangent.assign( triangleUVTangent( triStorage, hit.triangleIndex, hit.normal, material.normalTransform ) );
|
|
272
|
+
|
|
273
|
+
} );
|
|
274
|
+
|
|
275
|
+
const mapped = processNormal( hit.normal, material, uvCache, uvTangent ).toVar();
|
|
267
276
|
shadingNormal.assign( processBump( mapped, material, uvCache ) );
|
|
268
277
|
|
|
269
278
|
} );
|
package/src/TSL/Debugger.js
CHANGED
|
@@ -202,7 +202,7 @@ export const TraceDebugMode = Fn( ( [
|
|
|
202
202
|
|
|
203
203
|
// Sample all textures to get emissive
|
|
204
204
|
const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
205
|
-
material, hitInfo.uv, hitInfo.normal,
|
|
205
|
+
material, hitInfo.uv, hitInfo.normal, vec4( 0.0 ),
|
|
206
206
|
) ).toVar();
|
|
207
207
|
|
|
208
208
|
const emissiveColor = matSamples.emissive.toVar();
|
|
@@ -246,7 +246,7 @@ export const TraceDebugMode = Fn( ( [
|
|
|
246
246
|
|
|
247
247
|
// Get material-mapped normal (same as what's used in main shader)
|
|
248
248
|
const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
249
|
-
material, hitInfo.uv, hitInfo.normal,
|
|
249
|
+
material, hitInfo.uv, hitInfo.normal, vec4( 0.0 ),
|
|
250
250
|
) ).toVar();
|
|
251
251
|
|
|
252
252
|
const worldNormal = normalize( matSamples.normal );
|
|
@@ -292,7 +292,7 @@ export const TraceDebugMode = Fn( ( [
|
|
|
292
292
|
const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
|
|
293
293
|
|
|
294
294
|
const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
295
|
-
material, hitInfo.uv, hitInfo.normal,
|
|
295
|
+
material, hitInfo.uv, hitInfo.normal, vec4( 0.0 ),
|
|
296
296
|
) ).toVar();
|
|
297
297
|
|
|
298
298
|
const objectColor = matSamples.albedo.rgb;
|
|
@@ -318,7 +318,7 @@ export const TraceDebugMode = Fn( ( [
|
|
|
318
318
|
// Get primary surface material
|
|
319
319
|
const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
|
|
320
320
|
const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
321
|
-
material, hitInfo.uv, hitInfo.normal,
|
|
321
|
+
material, hitInfo.uv, hitInfo.normal, vec4( 0.0 ),
|
|
322
322
|
) ).toVar();
|
|
323
323
|
|
|
324
324
|
const albedoA = matSamples.albedo.rgb;
|
|
@@ -364,7 +364,7 @@ export const TraceDebugMode = Fn( ( [
|
|
|
364
364
|
// Bounce hit surface B — evaluate its contribution
|
|
365
365
|
const materialB = RayTracingMaterial.wrap( getMaterial( bounceHit.materialIndex, materialBuffer ) ).toVar();
|
|
366
366
|
const matSamplesB = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
367
|
-
materialB, bounceHit.uv, bounceHit.normal,
|
|
367
|
+
materialB, bounceHit.uv, bounceHit.normal, vec4( 0.0 ),
|
|
368
368
|
) ).toVar();
|
|
369
369
|
|
|
370
370
|
// Emissive contribution from surface B
|
|
@@ -141,57 +141,16 @@ export const sampleWavelengthForDispersion = Fn( ( [ baseIOR, dispersionStrength
|
|
|
141
141
|
// ABSORPTION
|
|
142
142
|
// ================================================================================
|
|
143
143
|
|
|
144
|
-
//
|
|
144
|
+
// Beer-Lambert transmittance over pathLength. attenuationDistance <= 0 (or non-finite,
|
|
145
|
+
// normalized to 0 by MaterialDataManager) means "no absorption".
|
|
145
146
|
export const calculateBeerLawAbsorption = /*@__PURE__*/ wgslFn( `
|
|
146
|
-
fn calculateBeerLawAbsorption( attenuationColor: vec3f, attenuationDistance: f32,
|
|
147
|
+
fn calculateBeerLawAbsorption( attenuationColor: vec3f, attenuationDistance: f32, pathLength: f32 ) -> vec3f {
|
|
147
148
|
if ( attenuationDistance <= 0.0f ) { return vec3f( 1.0f ); }
|
|
148
|
-
// Convert RGB attenuation color to absorption coefficients
|
|
149
149
|
let absorption = -log( max( attenuationColor, vec3f( 0.001f ) ) ) / attenuationDistance;
|
|
150
|
-
|
|
151
|
-
return exp( -absorption * thickness );
|
|
150
|
+
return exp( -absorption * pathLength );
|
|
152
151
|
}
|
|
153
152
|
` );
|
|
154
153
|
|
|
155
|
-
// ================================================================================
|
|
156
|
-
// SHADOW TRANSMITTANCE
|
|
157
|
-
// ================================================================================
|
|
158
|
-
|
|
159
|
-
export const calculateShadowTransmittance = Fn( ( [ rayDir, normal, material, entering ] ) => {
|
|
160
|
-
|
|
161
|
-
const n1 = select( entering, float( 1.0 ), material.ior ).toVar();
|
|
162
|
-
const n2 = select( entering, material.ior, float( 1.0 ) ).toVar();
|
|
163
|
-
|
|
164
|
-
const cosThetaI = abs( dot( normal, rayDir ) );
|
|
165
|
-
const sinThetaT2 = n1.mul( n1 ).div( n2.mul( n2 ) ).mul( float( 1.0 ).sub( cosThetaI.mul( cosThetaI ) ) );
|
|
166
|
-
|
|
167
|
-
// Handle total internal reflection
|
|
168
|
-
const result = float( 0.0 ).toVar();
|
|
169
|
-
|
|
170
|
-
If( sinThetaT2.lessThanEqual( 1.0 ), () => {
|
|
171
|
-
|
|
172
|
-
// Calculate Fresnel reflectance
|
|
173
|
-
const F0 = iorToFresnel0( n2, n1 );
|
|
174
|
-
const Fr = fresnelSchlickFloat( cosThetaI, F0 );
|
|
175
|
-
|
|
176
|
-
// Base transmission: what gets through after Fresnel reflection
|
|
177
|
-
const baseTransmission = float( 1.0 ).sub( Fr ).mul( material.transmission ).toVar();
|
|
178
|
-
|
|
179
|
-
// Apply Beer's law absorption for exiting rays
|
|
180
|
-
If( entering.not().and( material.attenuationDistance.greaterThan( 0.0 ) ), () => {
|
|
181
|
-
|
|
182
|
-
const absorption = calculateBeerLawAbsorption( { attenuationColor: material.attenuationColor, attenuationDistance: material.attenuationDistance, thickness: material.thickness } );
|
|
183
|
-
baseTransmission.assign( baseTransmission.mul( absorption.x.add( absorption.y ).add( absorption.z ).div( 3.0 ) ) );
|
|
184
|
-
|
|
185
|
-
} );
|
|
186
|
-
|
|
187
|
-
result.assign( clamp( baseTransmission, 0.0, 1.0 ) );
|
|
188
|
-
|
|
189
|
-
} );
|
|
190
|
-
|
|
191
|
-
return result;
|
|
192
|
-
|
|
193
|
-
} );
|
|
194
|
-
|
|
195
154
|
// ================================================================================
|
|
196
155
|
// MICROFACET TRANSMISSION
|
|
197
156
|
// ================================================================================
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { sampleEnvironment, sampleEquirectProbability, sampleEquirect, groundProjectedEnvDir } from './Environment.js';
|
|
18
18
|
import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial } from './Common.js';
|
|
19
19
|
import { cosineWeightedSample } from './MaterialSampling.js';
|
|
20
|
-
import { sampleAllMaterialTextures, processAnisotropyMap, applyExtensionMaps, getTransformedUV } from './TextureSampling.js';
|
|
20
|
+
import { sampleAllMaterialTextures, processAnisotropyMap, applyExtensionMaps, getTransformedUV, triangleUVTangent } from './TextureSampling.js';
|
|
21
21
|
import { evaluateMaterialResponse } from './MaterialEvaluation.js';
|
|
22
22
|
import { calculateDirectLightingUnified, calculateMaterialPDF } from './LightsSampling.js';
|
|
23
23
|
import { traceShadowRay, calculateRayOffset } from './LightsDirect.js';
|
|
@@ -616,8 +616,17 @@ export function buildShadeKernel( params ) {
|
|
|
616
616
|
}
|
|
617
617
|
);
|
|
618
618
|
|
|
619
|
+
// Normal-map tangent frame. Only paid for when there is a normal map: the arbitrary
|
|
620
|
+
// cross(up, N) fallback inside processNormal is what runs otherwise.
|
|
621
|
+
const uvTangent = vec4( 0.0 ).toVar();
|
|
622
|
+
If( material.normalMapIndex.greaterThanEqual( int( 0 ) ), () => {
|
|
623
|
+
|
|
624
|
+
uvTangent.assign( triangleUVTangent( triangleBuffer, int( hitTriIdx ), N, material.normalTransform ) );
|
|
625
|
+
|
|
626
|
+
} );
|
|
627
|
+
|
|
619
628
|
const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
620
|
-
material, samplingUV, N,
|
|
629
|
+
material, samplingUV, N, uvTangent,
|
|
621
630
|
) ).toVar();
|
|
622
631
|
|
|
623
632
|
// DDFA: snapshot the UNCLAMPED roughness (before the min-0.05 clamp below) for the specular↔diffuse
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, abs, atan, mix, clamp, texture, textureSize } from 'three/tsl';
|
|
1
|
+
import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, dot, length, sign, abs, atan, mix, clamp, texture, textureSize } from 'three/tsl';
|
|
2
2
|
import { DataArrayTexture, LinearFilter } from 'three';
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
MaterialSamples,
|
|
7
7
|
ExtMapResult,
|
|
8
8
|
} from './Struct.js';
|
|
9
|
+
import { getDatafromStorageBuffer } from './Common.js';
|
|
9
10
|
import { TEXTURE_CONSTANTS } from '../EngineDefaults.js';
|
|
10
11
|
|
|
11
12
|
// ================================================================================
|
|
@@ -382,7 +383,80 @@ export const processMetalnessRoughness = Fn( ( [ material, uvCache ] ) => {
|
|
|
382
383
|
|
|
383
384
|
} );
|
|
384
385
|
|
|
385
|
-
|
|
386
|
+
// (t[1][1], t[0][1]) — the row of the inverse 2x2 linear part that maps dP/du onto dP/du',
|
|
387
|
+
// up to the determinant, which drops out under the normalize that follows.
|
|
388
|
+
const uvTransformJacobian = /*@__PURE__*/ wgslFn( `
|
|
389
|
+
fn uvTransformJacobian( t: mat3x3f ) -> vec2f {
|
|
390
|
+
return vec2f( t[1][1], t[0][1] );
|
|
391
|
+
}
|
|
392
|
+
` );
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Build the UV tangent for one triangle, as vec4( T.xyz, handedness ).
|
|
396
|
+
*
|
|
397
|
+
* Returned in the *transformed* UV space of `transform`, since that is the space the normal
|
|
398
|
+
* map is sampled in — the inverse of the transform's 2x2 linear part maps dP/du back onto
|
|
399
|
+
* dP/du'. A pure repeat/scale leaves the direction alone; a rotated transform does not.
|
|
400
|
+
* w == 0 signals a degenerate parameterisation, which processNormal treats as "no frame".
|
|
401
|
+
*
|
|
402
|
+
* @param {Node} triangleBuffer packed triangle storage (8 vec4 stride)
|
|
403
|
+
* @param {Node} triIndex absolute triangle index
|
|
404
|
+
* @param {Node} geometryNormal shading-space normal, for the handedness test
|
|
405
|
+
* @param {Node} transform the normal map's mat3 UV transform
|
|
406
|
+
*/
|
|
407
|
+
export const triangleUVTangent = Fn( ( [ triangleBuffer, triIndex, geometryNormal, transform ] ) => {
|
|
408
|
+
|
|
409
|
+
const S = int( 8 );
|
|
410
|
+
const pA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), S ).xyz;
|
|
411
|
+
const e1 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), S ).xyz.sub( pA );
|
|
412
|
+
const e2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), S ).xyz.sub( pA );
|
|
413
|
+
|
|
414
|
+
const uvAB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 6 ), S );
|
|
415
|
+
const uvC = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 7 ), S ).xy;
|
|
416
|
+
const d1 = uvAB.zw.sub( uvAB.xy );
|
|
417
|
+
const d2 = uvC.sub( uvAB.xy );
|
|
418
|
+
|
|
419
|
+
const det = d1.x.mul( d2.y ).sub( d1.y.mul( d2.x ) );
|
|
420
|
+
const result = vec4( 0.0 ).toVar();
|
|
421
|
+
|
|
422
|
+
If( abs( det ).greaterThan( 1e-12 ), () => {
|
|
423
|
+
|
|
424
|
+
const r = float( 1.0 ).div( det );
|
|
425
|
+
const T = e1.mul( d2.y ).sub( e2.mul( d1.y ) ).mul( r ).toVar();
|
|
426
|
+
const B = e2.mul( d1.x ).sub( e1.mul( d2.x ) ).mul( r ).toVar();
|
|
427
|
+
|
|
428
|
+
// Re-express in the map's transformed UV space (inverse of the 2x2 linear part).
|
|
429
|
+
const uvJ = uvTransformJacobian( { t: transform } );
|
|
430
|
+
const Tt = T.mul( uvJ.x ).sub( B.mul( uvJ.y ) ).toVar();
|
|
431
|
+
|
|
432
|
+
If( length( Tt ).greaterThan( 1e-12 ), () => {
|
|
433
|
+
|
|
434
|
+
T.assign( Tt );
|
|
435
|
+
|
|
436
|
+
} );
|
|
437
|
+
|
|
438
|
+
If( length( T ).greaterThan( 1e-12 ), () => {
|
|
439
|
+
|
|
440
|
+
result.assign( vec4( normalize( T ), sign( dot( cross( geometryNormal, T ), B ) ) ) );
|
|
441
|
+
|
|
442
|
+
} );
|
|
443
|
+
|
|
444
|
+
} );
|
|
445
|
+
|
|
446
|
+
return result;
|
|
447
|
+
|
|
448
|
+
} );
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Per-triangle tangent frame from the UV parameterisation.
|
|
452
|
+
*
|
|
453
|
+
* `uvTangent` is vec4( T.xyz, handedness ); w == 0 means "no usable frame" (degenerate or
|
|
454
|
+
* missing UVs) and falls back to the arbitrary cross(up, N) frame. A frame unrelated to the
|
|
455
|
+
* texture's U axis rotates the whole perturbation: on a vertical fluted-glass panel the
|
|
456
|
+
* arbitrary frame comes out 90 deg off, so the rib normals tilt up/down instead of across
|
|
457
|
+
* the ribs.
|
|
458
|
+
*/
|
|
459
|
+
export const processNormal = Fn( ( [ geometryNormal, material, uvCache, uvTangent ] ) => {
|
|
386
460
|
|
|
387
461
|
const result = geometryNormal.toVar();
|
|
388
462
|
|
|
@@ -393,10 +467,25 @@ export const processNormal = Fn( ( [ geometryNormal, material, uvCache ] ) => {
|
|
|
393
467
|
normalMap.x.mulAssign( material.normalScale.x );
|
|
394
468
|
normalMap.y.assign( normalMap.y.negate().mul( material.normalScale.x ) );
|
|
395
469
|
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
470
|
+
const tangent = vec3( 0.0 ).toVar();
|
|
471
|
+
const handedness = float( 1.0 ).toVar();
|
|
472
|
+
|
|
473
|
+
// Gram-Schmidt against the interpolated normal, which is not exactly triangle-planar.
|
|
474
|
+
const perp = uvTangent.xyz.sub( geometryNormal.mul( dot( geometryNormal, uvTangent.xyz ) ) ).toVar();
|
|
475
|
+
|
|
476
|
+
If( abs( uvTangent.w ).greaterThan( 0.5 ).and( length( perp ).greaterThan( 1e-6 ) ), () => {
|
|
477
|
+
|
|
478
|
+
tangent.assign( normalize( perp ) );
|
|
479
|
+
handedness.assign( uvTangent.w );
|
|
480
|
+
|
|
481
|
+
} ).Else( () => {
|
|
482
|
+
|
|
483
|
+
const up = abs( geometryNormal.z ).lessThan( 0.999 ).select( vec3( 0.0, 0.0, 1.0 ), vec3( 1.0, 0.0, 0.0 ) );
|
|
484
|
+
tangent.assign( normalize( cross( up, geometryNormal ) ) );
|
|
485
|
+
|
|
486
|
+
} );
|
|
487
|
+
|
|
488
|
+
const bitangent = cross( geometryNormal, tangent ).mul( handedness );
|
|
400
489
|
|
|
401
490
|
result.assign( normalize(
|
|
402
491
|
tangent.mul( normalMap.x ).add( bitangent.mul( normalMap.y ) ).add( geometryNormal.mul( normalMap.z ) )
|
|
@@ -563,7 +652,7 @@ export const processEmissive = Fn( ( [ material, uvCache ] ) => {
|
|
|
563
652
|
// MAIN BATCHED SAMPLING FUNCTION
|
|
564
653
|
// ================================================================================
|
|
565
654
|
|
|
566
|
-
export const sampleAllMaterialTextures = Fn( ( [ material, uv, geometryNormal ] ) => {
|
|
655
|
+
export const sampleAllMaterialTextures = Fn( ( [ material, uv, geometryNormal, uvTangent ] ) => {
|
|
567
656
|
|
|
568
657
|
const albedo = vec4( 0.0 ).toVar();
|
|
569
658
|
const emissive = vec3( 0.0 ).toVar();
|
|
@@ -591,7 +680,7 @@ export const sampleAllMaterialTextures = Fn( ( [ material, uv, geometryNormal ]
|
|
|
591
680
|
metalness.assign( metalRough.x );
|
|
592
681
|
roughness.assign( metalRough.y );
|
|
593
682
|
|
|
594
|
-
const currentNormal = processNormal( geometryNormal, material, uvCache ).toVar();
|
|
683
|
+
const currentNormal = processNormal( geometryNormal, material, uvCache, uvTangent ).toVar();
|
|
595
684
|
normal.assign( processBump( currentNormal, material, uvCache ) );
|
|
596
685
|
|
|
597
686
|
emissive.assign( processEmissive( material, uvCache ) );
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
11
11
|
import { storage } from 'three/tsl';
|
|
12
|
-
import { MATERIAL_DATA_LAYOUT as M, TRIANGLE_DATA_LAYOUT as T } from '../EngineDefaults.js';
|
|
12
|
+
import { MATERIAL_DATA_LAYOUT as M, TRIANGLE_DATA_LAYOUT as T, normalizeAttenuationDistance } from '../EngineDefaults.js';
|
|
13
13
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
14
14
|
|
|
15
15
|
const log = createLogger( 'material' );
|
|
@@ -23,6 +23,32 @@ const TRI_BLOCKER_OFFSET = T.NORMAL_A_OFFSET + 3; // nA.w in shader (opaque-bloc
|
|
|
23
23
|
// Material properties that affect the shadow-ray opaque-blocker flag.
|
|
24
24
|
const BLOCKER_PROPS = new Set( [ 'transmission', 'transparent', 'opacity', 'alphaMode' ] );
|
|
25
25
|
|
|
26
|
+
// Scalar slots readable via getMaterialProperty().
|
|
27
|
+
const SCALAR_PROPERTY_OFFSETS = {
|
|
28
|
+
ior: M.IOR,
|
|
29
|
+
transmission: M.TRANSMISSION,
|
|
30
|
+
thickness: M.THICKNESS,
|
|
31
|
+
emissiveIntensity: M.EMISSIVE_INTENSITY,
|
|
32
|
+
attenuationDistance: M.ATTENUATION_DISTANCE,
|
|
33
|
+
opacity: M.OPACITY,
|
|
34
|
+
alphaTest: M.ALPHA_TEST,
|
|
35
|
+
metalness: M.METALNESS,
|
|
36
|
+
roughness: M.ROUGHNESS,
|
|
37
|
+
clearcoat: M.CLEARCOAT,
|
|
38
|
+
clearcoatRoughness: M.CLEARCOAT_ROUGHNESS,
|
|
39
|
+
dispersion: M.DISPERSION,
|
|
40
|
+
sheen: M.SHEEN,
|
|
41
|
+
sheenRoughness: M.SHEEN_ROUGHNESS,
|
|
42
|
+
specularIntensity: M.SPECULAR_INTENSITY,
|
|
43
|
+
iridescence: M.IRIDESCENCE,
|
|
44
|
+
iridescenceIOR: M.IRIDESCENCE_IOR,
|
|
45
|
+
subsurface: M.SUBSURFACE,
|
|
46
|
+
subsurfaceRadiusScale: M.SUBSURFACE_RADIUS_SCALE,
|
|
47
|
+
subsurfaceAnisotropy: M.SUBSURFACE_ANISOTROPY,
|
|
48
|
+
anisotropy: M.ANISOTROPY,
|
|
49
|
+
anisotropyRotation: M.ANISOTROPY_ROTATION, // radians, unlike the degrees hosts usually show
|
|
50
|
+
};
|
|
51
|
+
|
|
26
52
|
export class MaterialDataManager {
|
|
27
53
|
|
|
28
54
|
/**
|
|
@@ -179,6 +205,28 @@ export class MaterialDataManager {
|
|
|
179
205
|
|
|
180
206
|
// ===== MATERIAL PROPERTY UPDATES =====
|
|
181
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Read back a scalar material property from the storage buffer — the value the shader
|
|
210
|
+
* actually uses. Hosts need this because the engine resolves defaults the three.js
|
|
211
|
+
* material never carries (e.g. IOR on a MeshStandardMaterial), so a UI that falls back
|
|
212
|
+
* to its own default displays a number the renderer is not using.
|
|
213
|
+
* @param {number} materialIndex
|
|
214
|
+
* @param {string} property
|
|
215
|
+
* @returns {number|undefined} undefined if unavailable or not a scalar slot
|
|
216
|
+
*/
|
|
217
|
+
getMaterialProperty( materialIndex, property ) {
|
|
218
|
+
|
|
219
|
+
const data = this.materialStorageAttr?.array;
|
|
220
|
+
if ( ! data ) return undefined;
|
|
221
|
+
|
|
222
|
+
const offset = SCALAR_PROPERTY_OFFSETS[ property ];
|
|
223
|
+
if ( offset === undefined ) return undefined;
|
|
224
|
+
|
|
225
|
+
const index = materialIndex * M.FLOATS_PER_MATERIAL + offset;
|
|
226
|
+
return index < data.length ? data[ index ] : undefined;
|
|
227
|
+
|
|
228
|
+
}
|
|
229
|
+
|
|
182
230
|
/**
|
|
183
231
|
* Update a single material property in the storage buffer.
|
|
184
232
|
* @param {number} materialIndex
|
|
@@ -253,7 +301,7 @@ export class MaterialDataManager {
|
|
|
253
301
|
}
|
|
254
302
|
|
|
255
303
|
break;
|
|
256
|
-
case 'attenuationDistance': data[ stride + M.ATTENUATION_DISTANCE ] = value; break;
|
|
304
|
+
case 'attenuationDistance': data[ stride + M.ATTENUATION_DISTANCE ] = normalizeAttenuationDistance( value ); break;
|
|
257
305
|
case 'dispersion': data[ stride + M.DISPERSION ] = value; break;
|
|
258
306
|
case 'sheen': data[ stride + M.SHEEN ] = value; break;
|
|
259
307
|
case 'sheenRoughness': data[ stride + M.SHEEN_ROUGHNESS ] = value; break;
|
|
@@ -446,7 +494,7 @@ export class MaterialDataManager {
|
|
|
446
494
|
|
|
447
495
|
}
|
|
448
496
|
|
|
449
|
-
data[ stride + M.ATTENUATION_DISTANCE ] = materialData.attenuationDistance
|
|
497
|
+
data[ stride + M.ATTENUATION_DISTANCE ] = normalizeAttenuationDistance( materialData.attenuationDistance );
|
|
450
498
|
data[ stride + M.DISPERSION ] = materialData.dispersion ?? 0;
|
|
451
499
|
data[ stride + M.VISIBLE ] = 1; // Reserved slot (per-mesh visibility handled at BLAS-pointer level)
|
|
452
500
|
data[ stride + M.SHEEN ] = materialData.sheen ?? 0;
|