rayzee 8.5.0 → 8.6.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.
Files changed (60) hide show
  1. package/README.md +60 -4
  2. package/dist/{DLSSNeural-BNSJ8V3z.js → DLSSNeural-BhBCBgKf.js} +5 -5
  3. package/dist/{DLSSSuperRes-BoEW89Cl.js → DLSSSuperRes-BCA7F_k5.js} +5 -5
  4. package/dist/ToneMapGPU-BzcLheoR.js +533 -0
  5. package/dist/rayzee.es.js +27198 -9047
  6. package/dist/rayzee.umd.js +255 -187
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +15 -2
  9. package/src/Color/BakedViews.js +134 -0
  10. package/src/Color/BuiltinViews.js +261 -0
  11. package/src/Color/ColorManagement.js +1130 -0
  12. package/src/Color/ColorSpaces.js +432 -0
  13. package/src/Color/Displays.js +19 -0
  14. package/src/Color/InputColorSpaces.js +243 -0
  15. package/src/Color/LutBake.js +371 -0
  16. package/src/Color/OcioRuntime.js +338 -0
  17. package/src/Color/OcioViews.js +419 -0
  18. package/src/Color/ViewTransforms.js +323 -0
  19. package/src/Color/WorkingMatrix.js +86 -0
  20. package/src/EngineDefaults.js +55 -3
  21. package/src/EngineIssues.js +3 -0
  22. package/src/Passes/AIUpscaler.js +16 -7
  23. package/src/PathTracerApp.js +227 -7
  24. package/src/Processor/AssetLoader.js +1 -2
  25. package/src/Processor/EmissiveTriangleBuilder.js +34 -2
  26. package/src/Processor/GeometryExtractor.js +151 -138
  27. package/src/Processor/LightSerializer.js +8 -3
  28. package/src/Processor/MaterialPacking.js +168 -0
  29. package/src/Processor/PackedRayBuffer.js +6 -2
  30. package/src/Processor/ProceduralSky.js +4 -0
  31. package/src/Processor/SceneProcessor.js +25 -1
  32. package/src/Processor/SimpleSky.js +8 -0
  33. package/src/Processor/TextureCreator.js +137 -91
  34. package/src/Processor/TextureReadback.js +92 -0
  35. package/src/Processor/ToneMapCPU.js +80 -165
  36. package/src/Processor/ToneMapGPU.js +122 -151
  37. package/src/RenderSettings.js +1 -0
  38. package/src/Stages/Compositor.js +11 -2
  39. package/src/Stages/PathTracer.js +2 -0
  40. package/src/Stages/PathTracerStage.js +1 -1
  41. package/src/TSL/Common.js +15 -0
  42. package/src/TSL/Debugger.js +2 -3
  43. package/src/TSL/EmissiveSampling.js +6 -8
  44. package/src/TSL/ExtendKernel.js +9 -0
  45. package/src/TSL/Fresnel.js +23 -1
  46. package/src/TSL/HitFacet.js +102 -0
  47. package/src/TSL/LightsDirect.js +12 -33
  48. package/src/TSL/LightsSampling.js +15 -7
  49. package/src/TSL/MaterialEvaluation.js +26 -19
  50. package/src/TSL/MaterialProperties.js +74 -37
  51. package/src/TSL/MaterialTransmission.js +2 -3
  52. package/src/TSL/ShadeKernel.js +34 -21
  53. package/src/TSL/ShadowTerminator.js +125 -0
  54. package/src/TSL/Struct.js +9 -0
  55. package/src/TSL/Subsurface.js +2 -3
  56. package/src/index.js +29 -1
  57. package/src/managers/EnvironmentManager.js +20 -0
  58. package/src/managers/MaterialDataManager.js +118 -208
  59. package/src/managers/UniformManager.js +1 -0
  60. package/dist/ToneMapGPU-DzqSyzTx.js +0 -341
@@ -7,7 +7,7 @@
7
7
  import {
8
8
  Fn, float, vec2, vec3, vec4, int, uint,
9
9
  bool as tslBool,
10
- If, Loop, normalize, max, min, exp, log, clamp, dot, length, select, smoothstep, mix,
10
+ If, Loop, normalize, max, min, exp, log, clamp, dot, length, select, smoothstep,
11
11
  instanceIndex,
12
12
  sampler,
13
13
  atomicAdd, atomicLoad, atomicStore, uintBitsToFloat,
@@ -15,20 +15,21 @@ import {
15
15
  } from 'three/tsl';
16
16
 
17
17
  import { sampleEnvironment, sampleEquirectProbability, sampleEquirect, groundProjectedEnvDir } from './Environment.js';
18
- import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial } from './Common.js';
18
+ import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial, offsetRayOrigin, SHADOW_END } from './Common.js';
19
19
  import { cosineWeightedSample } from './MaterialSampling.js';
20
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
- import { traceShadowRay, calculateRayOffset } from './LightsDirect.js';
23
+ import { traceShadowRay } from './LightsDirect.js';
24
+ import { shadowTerminatorOrigin } from './ShadowTerminator.js';
25
+ import { unpackHitFacet } from './HitFacet.js';
24
26
  import { traverseBVHShadow } from './BVHTraversal.js';
25
27
  import { handleMaterialTransparency, MaterialInteractionResult } from './MaterialTransmission.js';
26
28
  import { sampleChromaticCollision, sampleHenyeyGreenstein, subsurfaceCoefficients, CollisionSample, MediumCoeffs } from './Subsurface.js';
27
29
  import { calculateIndirectLighting } from './LightsIndirect.js';
28
30
  import { IndirectLightingResult, sampleCone } from './LightsCore.js';
29
31
  import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette } from './PathTracerCore.js';
30
- import { evaluateDFG } from './MaterialProperties.js';
31
- import { dielectricF0 } from './Fresnel.js';
32
+ import { evaluateSpecularDFG, baseFresnelParams } from './MaterialProperties.js';
32
33
  import { NRD_HIT_DIST_A, NRD_HIT_DIST_B } from '../EngineDefaults.js';
33
34
  import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
34
35
  import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
@@ -46,6 +47,7 @@ import {
46
47
  MaterialCache,
47
48
  DirectLightingDual,
48
49
  DFGResult,
50
+ BaseFresnel,
49
51
  } from './Struct.js';
50
52
  import { getRandomSample, getRandomSample1D, getRandomSample2D, SAMPLER_DIMS_PER_BOUNCE, SAMPLER_DIM_AUX_BASE } from './Random.js';
51
53
  import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
@@ -56,7 +58,7 @@ import {
56
58
  readTransparentCount,
57
59
  readMisRayT,
58
60
  readHitDistance, readHitBarycentrics, readHitNormal,
59
- readHitMaterialIndex, readHitTriangleIndex, readHitInstanceLeaf,
61
+ readHitMaterialIndex, readHitTriangleIndex, readHitInstanceLeaf, readHitFacet,
60
62
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
61
63
  writeGBuffer, writeGBufferHitDist, readGBuffer, gbDecodeNormalDepth,
62
64
  readRayRadiance,
@@ -96,6 +98,7 @@ export function buildShadeKernel( params ) {
96
98
  globalIlluminationIntensity,
97
99
  cameraProjectionMatrix, cameraViewMatrix,
98
100
  fireflyThreshold, frame, resolution,
101
+ shadowTerminatorOffset, // Cycles' Shadow Terminator → Geometry Offset; 0 disables
99
102
  // Accumulation index, NOT the free-running seed counter `frame` rides. The firefly clamp
100
103
  // relaxes as sqrt(frame+1) so its bias decays as a render converges; keying that off the
101
104
  // seed axis let it grow for the whole session and disabled suppression outright.
@@ -283,6 +286,7 @@ export function buildShadeKernel( params ) {
283
286
  envTotalSum, envCompensationDelta, envResolution,
284
287
  enableEnvironmentLight,
285
288
  tslBool( true ), // wantUnoccluded
289
+ vec3( 0.0 ), planeN, float( 0.0 ),
286
290
  ) ).toVar();
287
291
 
288
292
  const lumShad = max( dot( dual.shadowed, REC709_LUMINANCE_COEFFICIENTS ), float( 0.0 ) );
@@ -930,10 +934,9 @@ export function buildShadeKernel( params ) {
930
934
 
931
935
  throughput.mulAssign( interaction.throughput );
932
936
 
933
- // reflection stays on same side, transmission pushes through
934
- const reflectOffsetDir = select( interaction.entering, N, N.negate() );
935
- const offsetDir = select( interaction.didReflect, reflectOffsetDir, direction );
936
- const newOrigin = hitPoint.add( offsetDir.mul( 0.001 ) );
937
+ // Off the side of the facet the new ray leaves on: reflection stays, transmission and alpha skip cross.
938
+ const Ng = unpackHitFacet( readHitFacet( hitBufferRO, rayID ) ).faceN;
939
+ const newOrigin = offsetRayOrigin( hitPoint, select( dot( Ng, interaction.direction ).lessThan( 0.0 ), Ng.negate(), Ng ) );
937
940
 
938
941
  // SSS = free bounce (depth unchanged); transmission advances camera-bounce depth.
939
942
  // Transmissive / alpha-skip / SSS-boundary are all FREE bounces — they do NOT advance camera depth (megakernel parity, gap #4). cameraDepth advances only on opaque scatter (below).
@@ -1062,6 +1065,14 @@ export function buildShadeKernel( params ) {
1062
1065
  // face-forwarded geometric normal: horizon guard for NEE and the bounce continuation
1063
1066
  const Ngeo = normalize( hitNormal );
1064
1067
  const NgeoFF = select( dot( Ngeo, V ).lessThan( 0.0 ), Ngeo.negate(), Ngeo ).toVar();
1068
+ // The facet on the viewer's side: every ray spawned from here is offset off it (HitFacet.js).
1069
+ const facet = unpackHitFacet( readHitFacet( hitBufferRO, rayID ) );
1070
+ const facetN = facet.faceN.toVar();
1071
+ const terminatorLift = Ngeo.mul( facet.liftScale );
1072
+ const lightShadowOrigin = L => shadowTerminatorOrigin( {
1073
+ hitPoint, offsetNormal: facetN, lift: terminatorLift, faceN: facetN, smoothN: NgeoFF, L,
1074
+ cutoff: shadowTerminatorOffset,
1075
+ } );
1065
1076
 
1066
1077
  // Two-sided shading: opaque path only (transmissive/SSS already continued). Decide the flip on the
1067
1078
  // GEOMETRIC normal — an inward-normal / double-sided mesh (GLB/PBRT) faces away as a whole — so a
@@ -1094,11 +1105,11 @@ export function buildShadeKernel( params ) {
1094
1105
  // smooth mirror / metal: DEFER — tint by the specular directional albedo (metal colour),
1095
1106
  // matching the opaque throughput (≈ F at the near-delta mirror lobe). Achromatic energy excluded.
1096
1107
  const NoV = max( dot( N, V ), float( 1e-3 ) );
1097
- const F0 = clamp(
1098
- mix( dielectricF0( material.ior ).mul( material.specularColor ), albedo, material.metalness ).mul( material.specularIntensity ),
1099
- vec3( 0.0 ), vec3( 1.0 ),
1100
- );
1101
- featCarry.mulAssign( DFGResult.wrap( evaluateDFG( F0, NoV, max( rawRough, float( 0.02 ) ) ) ).E_total );
1108
+ const bf = BaseFresnel.wrap( baseFresnelParams( material, albedo ) );
1109
+ featCarry.mulAssign( DFGResult.wrap( evaluateSpecularDFG(
1110
+ bf.f0, bf.f90, bf.eta, bf.F0m, material.metalness, vec3( 0.0 ), float( 0.0 ), bf.F0,
1111
+ NoV, max( rawRough, float( 0.02 ) ),
1112
+ ) ).E_total );
1102
1113
 
1103
1114
  } );
1104
1115
 
@@ -1176,6 +1187,7 @@ export function buildShadeKernel( params ) {
1176
1187
  envTotalSum, envCompensationDelta, envResolution,
1177
1188
  enableEnvironmentLight,
1178
1189
  tslBool( false ), // wantUnoccluded: false on real surfaces — dead-codes the unoccluded sum
1190
+ terminatorLift, facetN, shadowTerminatorOffset,
1179
1191
  ) ).shadowed.toVar();
1180
1192
 
1181
1193
  const giScale = select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) );
@@ -1230,11 +1242,12 @@ export function buildShadeKernel( params ) {
1230
1242
 
1231
1243
  If( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, NgeoFF ).greaterThan( 0.0 ) ), () => {
1232
1244
 
1233
- const rayOffset = calculateRayOffset( hitPoint, NgeoFF, material );
1234
- const rayOrigin = hitPoint.add( rayOffset );
1235
- const shadowDist = emissiveSample.distance.sub( 0.001 );
1245
+ // Aimed at the sampled point and stopped a relative hair short, as for area lights.
1246
+ const rayOrigin = lightShadowOrigin( emissiveSample.direction ).toVar();
1247
+ const toSample = emissiveSample.position.sub( rayOrigin ).toVar();
1248
+ const shadowDist = length( toSample ).toVar();
1236
1249
  const visibility = traceShadowRayWrapped(
1237
- rayOrigin, emissiveSample.direction, shadowDist,
1250
+ rayOrigin, toSample.div( shadowDist ), shadowDist.mul( SHADOW_END ),
1238
1251
  );
1239
1252
 
1240
1253
  If( visibility.greaterThan( 0.0 ), () => {
@@ -1278,7 +1291,7 @@ export function buildShadeKernel( params ) {
1278
1291
  lightBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
1279
1292
  triangleBuffer, bvhBuffer,
1280
1293
  traceShadowRayWrapped,
1281
- calculateRayOffset,
1294
+ Fn( ( [ p, , L ] ) => lightShadowOrigin( L ).sub( p ) ),
1282
1295
  );
1283
1296
 
1284
1297
  currentRadiance.assign( vec4(
@@ -1352,7 +1365,7 @@ export function buildShadeKernel( params ) {
1352
1365
 
1353
1366
  } );
1354
1367
 
1355
- const newOrigin = hitPoint.add( N.mul( 0.001 ) );
1368
+ const newOrigin = offsetRayOrigin( hitPoint, select( dot( facetN, bounceDir ).lessThan( 0.0 ), facetN.negate(), facetN ) );
1356
1369
 
1357
1370
  // Opaque scatter: the only bounce that advances camera depth.
1358
1371
  writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth.add( 1 ), sssSteps, transparentCount );
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Cycles' shadow terminator geometry offset (kernel/light/sample.h, Blender 5.1).
3
+ *
4
+ * Near the terminator of a smooth-shaded low-poly surface, a light's shadow ray leaving the flat
5
+ * facet is blocked by that facet or its neighbours although the smooth normal says the point is lit,
6
+ * which draws the mesh's outline into the shading. Starting the ray on the smooth surface the vertex
7
+ * normals describe — lifted along the normal by a parabolic estimate, bounded by each vertex's
8
+ * tangent plane — removes it. Only light and environment shadow rays use it, as in Cycles.
9
+ *
10
+ * Extend computes the lift with the facet normal (HitFacet.js), where the triangle's corners were just
11
+ * read, and packs both into the hit record's spare lane.
12
+ */
13
+
14
+ import { vec3, float, max, min, dot, length, clamp, select, If } from 'three/tsl';
15
+ import { instanceRows, instanceDirToWorld, offsetRayOrigin } from './Common.js';
16
+
17
+ /**
18
+ * The lift at full strength, as a signed length along the hit's smooth normal, from the triangle's
19
+ * corners (see HitFacet.js). Cycles lifts along the object-to-world image of that normal; the two
20
+ * agree unless the placement scales non-uniformly. Zero on a flat-shaded triangle.
21
+ *
22
+ * @param {Object} p - corners and vertex normals as stored (object space when `instanced`), and
23
+ * `faceN`, the world facet normal on the viewer's side
24
+ */
25
+ export function shadowTerminatorLift( { V0, V1, V2, N0, N1, N2, hitPoint, instanced, bvhBuffer, instanceLeaf, faceN } ) {
26
+
27
+ const scale = float( 0.0 ).toVar();
28
+
29
+ const P = hitPoint.toVar();
30
+ If( instanced, () => {
31
+
32
+ const rows = instanceRows( bvhBuffer, instanceLeaf );
33
+ P.assign( vec3(
34
+ rows[ 0 ].xyz.dot( hitPoint ).add( rows[ 0 ].w ),
35
+ rows[ 1 ].xyz.dot( hitPoint ).add( rows[ 1 ].w ),
36
+ rows[ 2 ].xyz.dot( hitPoint ).add( rows[ 2 ].w ),
37
+ ) );
38
+
39
+ } );
40
+
41
+ // The hit record keeps texture UVs, not barycentrics.
42
+ const e1 = V1.sub( V0 ).toVar();
43
+ const e2 = V2.sub( V0 ).toVar();
44
+ const ep = P.sub( V0 ).toVar();
45
+ const d11 = dot( e1, e1 );
46
+ const d12 = dot( e1, e2 );
47
+ const d22 = dot( e2, e2 );
48
+ const den = d11.mul( d22 ).sub( d12.mul( d12 ) ).toVar();
49
+
50
+ If( den.greaterThan( d11.mul( d22 ).mul( 1e-12 ) ), () => {
51
+
52
+ const dp1 = dot( ep, e1 );
53
+ const dp2 = dot( ep, e2 );
54
+ const v = clamp( d22.mul( dp1 ).sub( d12.mul( dp2 ) ).div( den ), 0.0, 1.0 ).toVar();
55
+ const w = clamp( d11.mul( dp2 ).sub( d12.mul( dp1 ) ).div( den ), 0.0, float( 1.0 ).sub( v ) ).toVar();
56
+ const u = float( 1.0 ).sub( v ).sub( w ).toVar();
57
+
58
+ const Pl = V0.mul( u ).add( V1.mul( v ) ).add( V2.mul( w ) ).toVar();
59
+ const nLocal = N0.mul( u ).add( N1.mul( v ) ).add( N2.mul( w ) ).toVar();
60
+ const n = nLocal.toVar();
61
+ If( instanced, () => {
62
+
63
+ const rows = instanceRows( bvhBuffer, instanceLeaf );
64
+ n.assign( instanceDirToWorld( { r0: rows[ 0 ].xyz, r1: rows[ 1 ].xyz, r2: rows[ 2 ].xyz, v: nLocal } ) );
65
+
66
+ } );
67
+
68
+ // Parabolic approximation.
69
+ const a = dot( N2.sub( N0 ), V0.sub( V2 ) );
70
+ const b = dot( N2.sub( N1 ), V1.sub( V2 ) );
71
+ const c = dot( N1.sub( N0 ), V1.sub( V0 ) );
72
+ const h = a.mul( u ).mul( u.sub( 1.0 ) ).add( a.add( b ).add( c ).mul( u ).mul( v ) ).add( b.mul( v ).mul( v.sub( 1.0 ) ) ).toVar();
73
+
74
+ // Bounded by the local linear envelope of the vertices' tangent planes.
75
+ If( dot( n, faceN ).greaterThan( 0.0 ), () => {
76
+
77
+ const h0 = max( dot( V0.sub( Pl ), N0 ).add( max( max( dot( V1.sub( V0 ), N0 ), dot( V2.sub( V0 ), N0 ) ), 0.0 ) ), 0.0 );
78
+ const h1 = max( dot( V1.sub( Pl ), N1 ).add( max( max( dot( V0.sub( V1 ), N1 ), dot( V2.sub( V1 ), N1 ) ), 0.0 ) ), 0.0 );
79
+ const h2 = max( dot( V2.sub( Pl ), N2 ).add( max( max( dot( V0.sub( V2 ), N2 ), dot( V1.sub( V2 ), N2 ) ), 0.0 ) ), 0.0 );
80
+ h.assign( max( min( min( h0, h1 ), h2 ), h.mul( 0.5 ) ) );
81
+
82
+ } ).Else( () => {
83
+
84
+ const h0 = max( dot( Pl.sub( V0 ), N0 ).add( max( max( dot( V0.sub( V1 ), N0 ), dot( V0.sub( V2 ), N0 ) ), 0.0 ) ), 0.0 );
85
+ const h1 = max( dot( Pl.sub( V1 ), N1 ).add( max( max( dot( V1.sub( V0 ), N1 ), dot( V1.sub( V2 ), N1 ) ), 0.0 ) ), 0.0 );
86
+ const h2 = max( dot( Pl.sub( V2 ), N2 ).add( max( max( dot( V2.sub( V0 ), N2 ), dot( V2.sub( V1 ), N2 ) ), 0.0 ) ), 0.0 );
87
+ h.assign( min( min( min( h0, h1 ), h2 ).negate(), h.mul( 0.5 ) ) );
88
+
89
+ } );
90
+
91
+ scale.assign( length( n ).mul( h ) );
92
+
93
+ } );
94
+
95
+ return scale;
96
+
97
+ }
98
+
99
+ /**
100
+ * A shadow ray's origin towards direction `L`: lifted by the share of the lift Cycles gives a
101
+ * direction this close to the terminator, then offset as every spawned ray is.
102
+ *
103
+ * @param {Object} p
104
+ * @param {Node} p.lift - the hit's unit smooth normal times the lift's scale
105
+ * @param {Node} p.smoothN - the interpolated normal on the viewer's side (Cycles' sd->N)
106
+ * @param {Node} p.cutoff - the geometry offset setting; 0 disables
107
+ */
108
+ export function shadowTerminatorOrigin( { hitPoint, offsetNormal, lift, faceN, smoothN, L, cutoff } ) {
109
+
110
+ // Cycles lifts towards the back for a transmitted direction; NEE here only lights the front.
111
+ const NL = dot( smoothN, L );
112
+ const NgL = dot( faceN, L );
113
+ const c = max( cutoff, 1e-6 );
114
+ const amount = select(
115
+ cutoff.greaterThan( 0.0 ).and( NL.greaterThan( 0.0 ) ),
116
+ select(
117
+ NL.lessThan( cutoff ),
118
+ clamp( float( 2.0 ).sub( NgL.add( NL ).div( c ) ), 0.0, 1.0 ),
119
+ clamp( float( 1.0 ).sub( NgL.div( c ) ), 0.0, 1.0 ),
120
+ ),
121
+ float( 0.0 ),
122
+ );
123
+ return offsetRayOrigin( hitPoint.add( lift.mul( amount ) ), offsetNormal );
124
+
125
+ }
package/src/TSL/Struct.js CHANGED
@@ -198,6 +198,15 @@ export const DFGResult = struct( {
198
198
  E_total: 'vec3', // clamp(E_ss * compensation, 0, 1)
199
199
  } );
200
200
 
201
+ // Base-layer Fresnel inputs (see baseFresnelParams).
202
+ export const BaseFresnel = struct( {
203
+ f0: 'vec3', // dielectric reflectance head-on: min(F0(ior) * specularColor, 1) * specularIntensity
204
+ f90: 'float', // dielectric reflectance at grazing: specularIntensity
205
+ eta: 'float', // max(ior, 1)
206
+ F0m: 'vec3', // metal F0: color * specularIntensity
207
+ F0: 'vec3', // the two blended by metalness — the multiscatter tint and the iridescence substrate
208
+ } );
209
+
201
210
  export const MaterialSamples = struct( {
202
211
  albedo: 'vec4',
203
212
  emissive: 'vec3',
@@ -30,7 +30,7 @@ import {
30
30
  import { struct } from './patches.js';
31
31
  import { TWO_PI, EPSILON, constructTBN } from './Common.js';
32
32
  import { getRandomSample1D, getRandomSample2D } from './Random.js';
33
- import { iorToFresnel0, fresnelSchlickFloat } from './Fresnel.js';
33
+ import { fresnelDielectric } from './Fresnel.js';
34
34
  import { ImportanceSampleGGX } from './MaterialSampling.js';
35
35
 
36
36
  // ================================================================================
@@ -189,8 +189,7 @@ export const handleSubsurfaceEntry = Fn( ( [
189
189
  const sinThetaT2 = n1.mul( n1 ).div( max( n2.mul( n2 ), EPSILON ) ).mul( float( 1.0 ).sub( cosThetaI.mul( cosThetaI ) ) );
190
190
  const tir = sinThetaT2.greaterThan( 1.0 ).toVar();
191
191
 
192
- const F0 = iorToFresnel0( n2, n1 );
193
- const Fr = select( tir, float( 1.0 ), fresnelSchlickFloat( cosThetaI, F0 ) ).toVar();
192
+ const Fr = fresnelDielectric( cosThetaI, n2.div( max( n1, EPSILON ) ) ).toVar();
194
193
  const reflectProb = clamp( Fr, 0.02, 0.98 ).toVar();
195
194
 
196
195
  const doReflect = tir.or( getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 11 ) ), rngState, resolution, frame ).lessThan( reflectProb ) ).toVar();
package/src/index.js CHANGED
@@ -27,7 +27,7 @@ export { SETTING_SOURCE } from './RenderSettings.js';
27
27
  export { configureAssets, getAssetConfig } from './AssetConfig.js';
28
28
 
29
29
  // glTF alphaMode derivation — the one definition, shared with hosts that edit materials
30
- export { deriveAlphaMode } from './Processor/GeometryExtractor.js';
30
+ export { deriveAlphaMode, MATERIAL_VALUE_SOURCE } from './Processor/GeometryExtractor.js';
31
31
 
32
32
  // Scene-level authoring metadata embedded in model files (glTF `extras`)
33
33
  export { extractSceneMetadata, parseSceneMetadata } from './Processor/SceneMetadata.js';
@@ -53,6 +53,7 @@ export {
53
53
  BVH_LEAF_MARKERS,
54
54
  TEXTURE_CONSTANTS,
55
55
  DEFAULT_TEXTURE_MATRIX,
56
+ MATERIAL_DEFAULTS,
56
57
  MEMORY_CONSTANTS,
57
58
  PRODUCTION_RENDER_CONFIG,
58
59
  INTERACTIVE_RENDER_CONFIG,
@@ -103,3 +104,30 @@ export { InteractionManager } from './managers/InteractionManager.js';
103
104
  // host needs to build UI against.
104
105
  export { renderUpscaled, SR_SCALE, SR_MAX_INPUT } from './dlss/DLSSSuperRes.js';
105
106
  export { DLSS_NR_DEFAULTS, DLSS_NR_RANGES, DLSS_NR_MAX_PIXELS } from './dlss/DLSSNeural.js';
107
+
108
+ // ── Colour management ───────────────────────────────────────────────────────────────────────────
109
+ // `app.color` is the instance a host normally uses; these are for building UI against it, and for
110
+ // code that needs the view-transform registry without an app.
111
+ export {
112
+ ColorManagement, getActiveColorManagement, setActiveColorManagement,
113
+ isColorManaged, DEFAULT_WORKING_SPACE, findNativeLinearSpace, canvasColorSpaceFor,
114
+ } from './Color/ColorManagement.js';
115
+ export { displayCanvasFit } from './Color/Displays.js';
116
+ export {
117
+ VIEW_TRANSFORMS, listViewTransforms, getViewTransform, addViewTransform, removeViewTransform,
118
+ onRegistryChange, getRegistryVersion, buildToneMapWGSL,
119
+ OCIO_VIEW_BASE, MAX_TABLE_TRANSFORMS,
120
+ } from './Color/ViewTransforms.js';
121
+ export {
122
+ buildOcioView, addOcioView, addAllOcioViews,
123
+ DEFAULT_LUT_SIZE, DEFAULT_MIN_EV, DEFAULT_MAX_EV,
124
+ } from './Color/OcioViews.js';
125
+ export {
126
+ resolveInputSpace, textureInputSpace, setInputOverride, clearInputOverrides,
127
+ listInputOverrides, listFileRules, isDataTexture,
128
+ } from './Color/InputColorSpaces.js';
129
+ export {
130
+ convertColor, convertPixelsF32, convertEncodedRGBA8, applyMatrixRGBA8, extractMatrix,
131
+ hasColorSpace, isDataSpace, srgbToLinear, linearToSrgb,
132
+ } from './Color/ColorSpaces.js';
133
+ export { measureBakeError, bakeLut, makeCpuSampler } from './Color/LutBake.js';
@@ -17,6 +17,7 @@ import { createLogger, fmt } from '../utils/Logger.js';
17
17
 
18
18
  const log = createLogger( 'env' );
19
19
  import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
20
+ import { getActiveColorManagement } from '../Color/ColorManagement.js';
20
21
 
21
22
  export class EnvironmentManager {
22
23
 
@@ -234,6 +235,25 @@ export class EnvironmentManager {
234
235
 
235
236
  if ( ! envTex ) return;
236
237
 
238
+ // Before the CDF is built, so importance sampling sees the same pixels the shader will.
239
+ // Inert unless a working space has been adopted; the texture's own space is honoured when
240
+ // the host tagged it, otherwise it is taken as linear Rec.709, which is what every HDRI
241
+ // loader produces.
242
+ const cm = getActiveColorManagement();
243
+ if ( cm?.hasConfig ) {
244
+
245
+ try {
246
+
247
+ cm.convertTexturePixels( envTex, { space: envTex.userData?.ocioColorSpace ?? null } );
248
+
249
+ } catch ( error ) {
250
+
251
+ log.warn( `environment colour conversion failed, left in its original space: ${error.message}` );
252
+
253
+ }
254
+
255
+ }
256
+
237
257
  this.environmentTexture = envTex;
238
258
  this.envTexSize.set( envTex.image.width, envTex.image.height );
239
259