rayzee 7.30.2 → 7.31.1
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/README.md +113 -2
- package/dist/rayzee.es.js +2733 -2251
- package/dist/rayzee.umd.js +232 -7
- package/package.json +1 -1
- package/src/EngineDefaults.js +9 -1
- package/src/EngineIssues.js +1 -0
- package/src/Passes/OIDNDenoiser.js +497 -347
- package/src/PathTracerApp.js +23 -3
- package/src/Processor/AssetLoader.js +106 -13
- package/src/Processor/PBRT/EqualAreaOctahedral.js +149 -0
- package/src/Processor/PBRT/LoopSubdivision.js +240 -0
- package/src/Processor/PBRT/PBRTMath.js +9 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -1
- package/src/Processor/PBRT/index.js +63 -9
- package/src/Processor/SceneProcessor.js +17 -1
- package/src/Processor/ToneMapWGSL.js +162 -0
- package/src/managers/CameraManager.js +11 -0
- package/src/managers/DenoisingManager.js +251 -22
- package/src/managers/OverlayManager.js +4 -0
- package/src/managers/VideoRenderManager.js +5 -5
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
Group, Mesh, PerspectiveCamera, Matrix4, Vector3,
|
|
18
|
-
BufferGeometry, Float32BufferAttribute, SphereGeometry,
|
|
18
|
+
BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute, SphereGeometry,
|
|
19
19
|
DataTexture, FloatType, RGBAFormat, LinearFilter, EquirectangularReflectionMapping
|
|
20
20
|
} from 'three';
|
|
21
21
|
import { buildMaterial, pFloat, pString, resolveSpectrum } from './PBRTMaterials.js';
|
|
22
|
+
import { loopSubdivide } from './LoopSubdivision.js';
|
|
23
|
+
import { octahedralToEquirect } from './EqualAreaOctahedral.js';
|
|
22
24
|
import * as M from './PBRTMath.js';
|
|
23
25
|
|
|
24
26
|
const FLIP_Z = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, 1 ];
|
|
@@ -187,6 +189,7 @@ export class PBRTSceneBuilder {
|
|
|
187
189
|
|
|
188
190
|
case 'trianglemesh': return this._triangleMesh( shape.params );
|
|
189
191
|
case 'bilinearmesh': return this._bilinearMesh( shape.params );
|
|
192
|
+
case 'loopsubdiv': return this._loopSubdiv( shape.params );
|
|
190
193
|
case 'plymesh': return this._plyMesh( shape.params );
|
|
191
194
|
case 'sphere': return this._sphere( shape.params );
|
|
192
195
|
case 'disk': return this._disk( shape.params );
|
|
@@ -224,6 +227,33 @@ export class PBRTSceneBuilder {
|
|
|
224
227
|
|
|
225
228
|
}
|
|
226
229
|
|
|
230
|
+
_loopSubdiv( params ) {
|
|
231
|
+
|
|
232
|
+
const P = params.P?.value;
|
|
233
|
+
const indices = params.indices?.value;
|
|
234
|
+
if ( ! P || P.length < 9 || ! indices || indices.length < 3 ) {
|
|
235
|
+
|
|
236
|
+
this.warn( 'loopsubdiv missing P/indices' ); return null;
|
|
237
|
+
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// pbrt's default is 3; each level quadruples the face count, so a deep level on
|
|
241
|
+
// a dense cage is worth refusing rather than stalling the import.
|
|
242
|
+
const requested = Math.max( 0, Math.min( 6, Math.round( pFloat( params, 'levels', 3 ) ) ) );
|
|
243
|
+
const { positions, indices: faces, levels } = loopSubdivide( P, indices, requested );
|
|
244
|
+
|
|
245
|
+
if ( levels < requested ) this.warn(
|
|
246
|
+
`loopsubdiv refined ${levels}/${requested} levels — the control mesh is too dense for the triangle budget`
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const geo = new BufferGeometry();
|
|
250
|
+
geo.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
|
|
251
|
+
geo.setIndex( new Uint32BufferAttribute( faces, 1 ) );
|
|
252
|
+
geo.computeVertexNormals();
|
|
253
|
+
return geo;
|
|
254
|
+
|
|
255
|
+
}
|
|
256
|
+
|
|
227
257
|
// Bilinear patch mesh → triangulate each quad (P + indices in quads of 4).
|
|
228
258
|
_bilinearMesh( params ) {
|
|
229
259
|
|
|
@@ -487,6 +517,16 @@ export class PBRTSceneBuilder {
|
|
|
487
517
|
camera.up.copy( up.normalize() );
|
|
488
518
|
camera.position.copy( eye );
|
|
489
519
|
camera.lookAt( target );
|
|
520
|
+
|
|
521
|
+
// pbrt's camera space is left-handed: image-right is cameraToWorld's first column,
|
|
522
|
+
// = cross(up, dir). three's lookAt() builds cross(up, -dir) — the opposite — so a
|
|
523
|
+
// plain pbrt scene imports left-right flipped against pbrt's own render. An
|
|
524
|
+
// exported scene's `Scale -1 1 1` already negates that column (det < 0), and then
|
|
525
|
+
// lookAt() happens to agree and no correction is wanted. Hence: mirror exactly when
|
|
526
|
+
// the scene does NOT. Verified against the reference images for contemporary-bathroom
|
|
527
|
+
// (has the Scale) and killeroos/killeroo-simple (does not).
|
|
528
|
+
if ( ( M.determinant3( cam.cameraToWorld ) > 0 ) !== this.convertHandedness ) camera.scale.x = - 1;
|
|
529
|
+
|
|
490
530
|
camera.updateMatrixWorld( true );
|
|
491
531
|
return camera;
|
|
492
532
|
|
|
@@ -506,6 +546,44 @@ export class PBRTSceneBuilder {
|
|
|
506
546
|
|
|
507
547
|
// ── lights / environment ───────────────────────────────────────
|
|
508
548
|
|
|
549
|
+
/**
|
|
550
|
+
* A square infinite-light image is pbrt's equal-area octahedral layout, not a
|
|
551
|
+
* lat-long panorama — resample it, folding in the light's transform and `scale`.
|
|
552
|
+
* Returns null when the image is not square (nothing to convert) or its pixels are
|
|
553
|
+
* not readable, leaving the caller's equirectangular path in place.
|
|
554
|
+
*/
|
|
555
|
+
_equirectFromInfiniteLight( tex, inf, scale, filename ) {
|
|
556
|
+
|
|
557
|
+
const image = tex.image;
|
|
558
|
+
const data = image?.data;
|
|
559
|
+
if ( ! data || ! image.width || image.width !== image.height ) {
|
|
560
|
+
|
|
561
|
+
if ( image?.width !== image?.height ) this.warn(
|
|
562
|
+
`infinite-light image "${filename}" is ${image?.width}x${image?.height}, not square — ` +
|
|
563
|
+
'read as equirectangular, but pbrt would read it as equal-area octahedral'
|
|
564
|
+
);
|
|
565
|
+
return null;
|
|
566
|
+
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const channels = data.length / ( image.width * image.height );
|
|
570
|
+
if ( ! Number.isInteger( channels ) || channels < 3 ) return null;
|
|
571
|
+
|
|
572
|
+
// three's EXR/HDR loaders fill DataTexture rows bottom-up; pbrt reads the square top-down.
|
|
573
|
+
const { data: pixels, width, height } = octahedralToEquirect(
|
|
574
|
+
{ data, width: image.width, height: image.height, channels, bottomUp: true },
|
|
575
|
+
inf.ctm, scale
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const out = new DataTexture( pixels, width, height, RGBAFormat, FloatType );
|
|
579
|
+
out.mapping = EquirectangularReflectionMapping;
|
|
580
|
+
out.minFilter = LinearFilter;
|
|
581
|
+
out.magFilter = LinearFilter;
|
|
582
|
+
out.needsUpdate = true;
|
|
583
|
+
return out;
|
|
584
|
+
|
|
585
|
+
}
|
|
586
|
+
|
|
509
587
|
async _buildEnvironment( lights ) {
|
|
510
588
|
|
|
511
589
|
const inf = lights.find( l => l.type === 'infinite' );
|
|
@@ -521,6 +599,9 @@ export class PBRTSceneBuilder {
|
|
|
521
599
|
const tex = await this.resolveEnvironment( filename );
|
|
522
600
|
if ( tex ) {
|
|
523
601
|
|
|
602
|
+
const converted = this._equirectFromInfiniteLight( tex, inf, scale, filename );
|
|
603
|
+
if ( converted ) return { texture: converted };
|
|
604
|
+
|
|
524
605
|
tex.mapping = EquirectangularReflectionMapping;
|
|
525
606
|
return { texture: tex };
|
|
526
607
|
|
|
@@ -91,24 +91,78 @@ class VirtualFS {
|
|
|
91
91
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
94
|
+
/** Decoded text of a zip entry, or '' when the value is not decodable bytes. */
|
|
95
|
+
function entryText( bytes ) {
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
|
|
99
|
+
return decoder.decode( bytes );
|
|
100
|
+
|
|
101
|
+
} catch {
|
|
102
|
+
|
|
103
|
+
return '';
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Rank every .pbrt in the archive that could be a top-level scene, best first.
|
|
111
|
+
*
|
|
112
|
+
* Name shape alone is not enough: a scene's `geometry.pbrt` fragment carries a shorter
|
|
113
|
+
* name than the real entry and would win on sort, loading every shape with an unresolved
|
|
114
|
+
* NamedMaterial and no camera or lights. Content decides first — a fragment has no
|
|
115
|
+
* `WorldBegin` and is named by someone else's Include — and the name only breaks ties.
|
|
116
|
+
*
|
|
117
|
+
* More than one survivor is a real archive shape, not an error: transparent-machines
|
|
118
|
+
* ships five independent frames side by side. Callers should say which one they took.
|
|
119
|
+
*/
|
|
120
|
+
export function listEntryPaths( entries ) {
|
|
96
121
|
|
|
97
122
|
const pbrts = Object.keys( entries ).filter( k => k.toLowerCase().endsWith( '.pbrt' ) );
|
|
98
|
-
if ( pbrts.length
|
|
123
|
+
if ( pbrts.length <= 1 ) return pbrts;
|
|
124
|
+
|
|
125
|
+
const texts = new Map( pbrts.map( k => [ k, entryText( entries[ k ] ) ] ) );
|
|
99
126
|
|
|
100
|
-
const
|
|
101
|
-
const
|
|
127
|
+
const included = new Set();
|
|
128
|
+
for ( const text of texts.values() ) {
|
|
102
129
|
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
for ( const m of text.matchAll( /^[ \t]*(?:Include|Import)[ \t]+"([^"]+)"/gm ) ) {
|
|
131
|
+
|
|
132
|
+
included.add( normalizePath( m[ 1 ] ).toLowerCase().split( '/' ).pop() );
|
|
133
|
+
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const narrow = ( candidates, keep ) => {
|
|
139
|
+
|
|
140
|
+
const kept = candidates.filter( keep );
|
|
141
|
+
return kept.length ? kept : candidates;
|
|
142
|
+
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let pool = narrow( pbrts, k => /^[ \t]*WorldBegin\b/m.test( texts.get( k ) ) );
|
|
146
|
+
pool = narrow( pool, k => ! included.has( k.toLowerCase().split( '/' ).pop() ) );
|
|
147
|
+
pool = narrow( pool, k => /(^|\/)(scene|main)\.pbrt$/i.test( k ) );
|
|
148
|
+
|
|
149
|
+
// Shallowest, then shortest name, then the path itself — the last key keeps the
|
|
150
|
+
// order independent of however the zip happened to enumerate its entries.
|
|
151
|
+
return pool.sort( ( a, b ) => {
|
|
105
152
|
|
|
106
153
|
const da = a.split( '/' ).length, db = b.split( '/' ).length;
|
|
107
|
-
|
|
154
|
+
if ( da !== db ) return da - db;
|
|
155
|
+
if ( a.length !== b.length ) return a.length - b.length;
|
|
156
|
+
return a < b ? - 1 : a > b ? 1 : 0;
|
|
108
157
|
|
|
109
158
|
} );
|
|
110
159
|
|
|
111
|
-
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Best top-level .pbrt entry, or null when the archive has none. */
|
|
163
|
+
export function pickEntryPath( entries ) {
|
|
164
|
+
|
|
165
|
+
return listEntryPaths( entries )[ 0 ] || null;
|
|
112
166
|
|
|
113
167
|
}
|
|
114
168
|
|
|
@@ -931,11 +931,27 @@ export class SceneProcessor {
|
|
|
931
931
|
this._linearTexPacked = new Map();
|
|
932
932
|
|
|
933
933
|
let bucketOverflowReported = false;
|
|
934
|
+
let undecodedReported = false;
|
|
934
935
|
|
|
935
936
|
// Assign one texture to its (bucket, layer) within a pool; dedup by source uuid.
|
|
936
937
|
const assign = ( tex, lists, dedup, flat, shapes ) => {
|
|
937
938
|
|
|
938
|
-
if ( ! tex
|
|
939
|
+
if ( ! tex ) return - 1;
|
|
940
|
+
|
|
941
|
+
// A map whose image has not landed yet is indistinguishable here from no map at
|
|
942
|
+
// all, and the material would render untextured with nothing in the log.
|
|
943
|
+
if ( ! tex.image ) {
|
|
944
|
+
|
|
945
|
+
if ( ! undecodedReported ) this.config.issues?.record(
|
|
946
|
+
ISSUE_CODES.TEXTURE_BUILD_FAILED,
|
|
947
|
+
'a material map had not finished decoding when the scene was packed; it renders untextured',
|
|
948
|
+
{ texture: tex.name || tex.source?.uuid || tex.uuid }
|
|
949
|
+
);
|
|
950
|
+
undecodedReported = true;
|
|
951
|
+
return - 1;
|
|
952
|
+
|
|
953
|
+
}
|
|
954
|
+
|
|
939
955
|
const bucket = getTextureBucketId( tex.image.width, tex.image.height, shapes );
|
|
940
956
|
const uuid = tex.source?.uuid ?? tex.uuid;
|
|
941
957
|
const seen = dedup[ bucket ].get( uuid );
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGSL tone mapping, a transcription of ToneMapCPU.js.
|
|
3
|
+
*
|
|
4
|
+
* The two must stay identical: the same image is produced on the CPU by `toneMapToRGBA8`
|
|
5
|
+
* (renderToBuffer, upscaler) and on the GPU by this (the OIDN output pack). Order is part of
|
|
6
|
+
* the contract — exposure, saturation, clamp-negative, curve, sRGB OETF — and moving any step
|
|
7
|
+
* shifts every mid-tone.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
NoToneMapping, LinearToneMapping, ReinhardToneMapping,
|
|
11
|
+
CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping
|
|
12
|
+
} from 'three';
|
|
13
|
+
|
|
14
|
+
const SUPPORTED = new Set( [
|
|
15
|
+
NoToneMapping, LinearToneMapping, ReinhardToneMapping,
|
|
16
|
+
CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping
|
|
17
|
+
] );
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Three.js ToneMapping constant → shader mode. Anything unsupported falls back to ACES,
|
|
21
|
+
* matching `TONE_MAP_FNS.get( x ) || TONE_MAP_FNS.get( ACESFilmicToneMapping )`.
|
|
22
|
+
* @param {number} toneMapping
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function toneMapMode( toneMapping ) {
|
|
26
|
+
|
|
27
|
+
return SUPPORTED.has( toneMapping ) ? toneMapping : ACESFilmicToneMapping;
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prelude defining `toneMapPixel( linear, gain, saturation, mode ) -> vec3f` (sRGB, [0,1]).
|
|
33
|
+
* Concatenate ahead of a kernel that wants it.
|
|
34
|
+
*/
|
|
35
|
+
export const TONE_MAP_WGSL = /* wgsl */`
|
|
36
|
+
fn tmSaturation( c: vec3f, saturation: f32 ) -> vec3f {
|
|
37
|
+
if ( saturation == 1.0 ) { return c; }
|
|
38
|
+
let luma = dot( c, vec3f( 0.2126, 0.7152, 0.0722 ) );
|
|
39
|
+
return vec3f( luma ) + ( c - vec3f( luma ) ) * saturation;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn tmSrgb1( c: f32 ) -> f32 {
|
|
43
|
+
if ( c <= 0.0031308 ) { return 12.92 * c; }
|
|
44
|
+
return 1.055 * pow( c, 1.0 / 2.4 ) - 0.055;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn tmSrgb( c: vec3f ) -> vec3f {
|
|
48
|
+
return vec3f( tmSrgb1( c.r ), tmSrgb1( c.g ), tmSrgb1( c.b ) );
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn tmReinhard( c: vec3f ) -> vec3f {
|
|
52
|
+
return clamp( c / ( c + vec3f( 1.0 ) ), vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn tmCineon1( x: f32 ) -> f32 {
|
|
56
|
+
let c = max( x - 0.004, 0.0 );
|
|
57
|
+
return pow( ( c * ( 6.2 * c + 0.5 ) ) / ( c * ( 6.2 * c + 1.7 ) + 0.06 ), 2.2 );
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn tmCineon( c: vec3f ) -> vec3f {
|
|
61
|
+
return vec3f( tmCineon1( c.r ), tmCineon1( c.g ), tmCineon1( c.b ) );
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn tmAcesFit( c: f32 ) -> f32 {
|
|
65
|
+
return ( c * ( c + 0.0245786 ) - 0.000090537 ) / ( c * ( 0.983729 * c + 0.4329510 ) + 0.238081 );
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn tmAces( cIn: vec3f ) -> vec3f {
|
|
69
|
+
let c = cIn / 0.6;
|
|
70
|
+
var i = vec3f(
|
|
71
|
+
0.59719 * c.r + 0.35458 * c.g + 0.04823 * c.b,
|
|
72
|
+
0.07600 * c.r + 0.90834 * c.g + 0.01566 * c.b,
|
|
73
|
+
0.02840 * c.r + 0.13383 * c.g + 0.83777 * c.b
|
|
74
|
+
);
|
|
75
|
+
i = vec3f( tmAcesFit( i.r ), tmAcesFit( i.g ), tmAcesFit( i.b ) );
|
|
76
|
+
let o = vec3f(
|
|
77
|
+
1.60475 * i.r - 0.53108 * i.g - 0.07367 * i.b,
|
|
78
|
+
-0.10208 * i.r + 1.10813 * i.g - 0.00605 * i.b,
|
|
79
|
+
-0.00327 * i.r - 0.07276 * i.g + 1.07602 * i.b
|
|
80
|
+
);
|
|
81
|
+
return clamp( o, vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn tmAgxApprox( x: f32 ) -> f32 {
|
|
85
|
+
let x2 = x * x;
|
|
86
|
+
let x4 = x2 * x2;
|
|
87
|
+
return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn tmAgx( r: vec3f ) -> vec3f {
|
|
91
|
+
let c = vec3f(
|
|
92
|
+
0.6274 * r.r + 0.3293 * r.g + 0.0433 * r.b,
|
|
93
|
+
0.0691 * r.r + 0.9195 * r.g + 0.0113 * r.b,
|
|
94
|
+
0.0164 * r.r + 0.0880 * r.g + 0.8956 * r.b
|
|
95
|
+
);
|
|
96
|
+
var a = vec3f(
|
|
97
|
+
0.856627153315983 * c.r + 0.0951212405381588 * c.g + 0.0482516061458583 * c.b,
|
|
98
|
+
0.137318972929847 * c.r + 0.761241990602591 * c.g + 0.101439036467562 * c.b,
|
|
99
|
+
0.11189821299995 * c.r + 0.0767994186031903 * c.g + 0.811302368396859 * c.b
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
let agxMinEv = -12.47393;
|
|
103
|
+
let agxMaxEv = 4.026069;
|
|
104
|
+
let range = agxMaxEv - agxMinEv;
|
|
105
|
+
a = clamp( ( log2( max( a, vec3f( 1e-10 ) ) ) - vec3f( agxMinEv ) ) / range, vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
106
|
+
a = vec3f( tmAgxApprox( a.r ), tmAgxApprox( a.g ), tmAgxApprox( a.b ) );
|
|
107
|
+
|
|
108
|
+
var o = vec3f(
|
|
109
|
+
1.1271005818144368 * a.r - 0.11060664309660323 * a.g - 0.016493938717834573 * a.b,
|
|
110
|
+
-0.1413297634984383 * a.r + 1.157823702216272 * a.g - 0.016493938717834257 * a.b,
|
|
111
|
+
-0.14132976349843826 * a.r - 0.11060664309660294 * a.g + 1.2519364065950405 * a.b
|
|
112
|
+
);
|
|
113
|
+
o = pow( max( o, vec3f( 0.0 ) ), vec3f( 2.2 ) );
|
|
114
|
+
|
|
115
|
+
return clamp( vec3f(
|
|
116
|
+
1.6605 * o.r - 0.5876 * o.g - 0.0728 * o.b,
|
|
117
|
+
-0.1246 * o.r + 1.1329 * o.g - 0.0083 * o.b,
|
|
118
|
+
-0.0182 * o.r - 0.1006 * o.g + 1.1187 * o.b
|
|
119
|
+
), vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn tmNeutral( cIn: vec3f ) -> vec3f {
|
|
123
|
+
let startCompression = 0.8 - 0.04;
|
|
124
|
+
let desaturation = 0.15;
|
|
125
|
+
|
|
126
|
+
let x = min( cIn.r, min( cIn.g, cIn.b ) );
|
|
127
|
+
var offset = 0.04;
|
|
128
|
+
if ( x < 0.08 ) { offset = x - 6.25 * x * x; }
|
|
129
|
+
|
|
130
|
+
var c = cIn - vec3f( offset );
|
|
131
|
+
let peak = max( c.r, max( c.g, c.b ) );
|
|
132
|
+
if ( peak < startCompression ) { return c; }
|
|
133
|
+
|
|
134
|
+
let d = 1.0 - startCompression;
|
|
135
|
+
let newPeak = 1.0 - d * d / ( peak + d - startCompression );
|
|
136
|
+
c = c * ( newPeak / peak );
|
|
137
|
+
let gFactor = 1.0 - 1.0 / ( desaturation * ( peak - newPeak ) + 1.0 );
|
|
138
|
+
|
|
139
|
+
return c + ( vec3f( newPeak ) - c ) * gFactor;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn tmCurve( cIn: vec3f, mode: u32 ) -> vec3f {
|
|
143
|
+
// Three.js clamps the fragment output before tone mapping ("force unsigned floats"), and
|
|
144
|
+
// AgX/Neutral mix negatives across channels rather than clipping them.
|
|
145
|
+
let c = max( cIn, vec3f( 0.0 ) );
|
|
146
|
+
switch mode {
|
|
147
|
+
case 0u, 1u: { return clamp( c, vec3f( 0.0 ), vec3f( 1.0 ) ); }
|
|
148
|
+
case 2u: { return tmReinhard( c ); }
|
|
149
|
+
case 3u: { return tmCineon( c ); }
|
|
150
|
+
case 6u: { return tmAgx( c ); }
|
|
151
|
+
case 7u: { return tmNeutral( c ); }
|
|
152
|
+
default: { return tmAces( c ); }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn toneMapPixel( linear: vec3f, gain: f32, saturation: f32, mode: u32 ) -> vec3f {
|
|
157
|
+
var c = linear * gain;
|
|
158
|
+
c = tmSaturation( c, saturation );
|
|
159
|
+
c = tmCurve( c, mode );
|
|
160
|
+
return tmSrgb( c );
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
@@ -3,6 +3,8 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
3
3
|
import { EngineEvents, } from '../EngineEvents.js';
|
|
4
4
|
import { AF_DEFAULTS } from '../EngineDefaults.js';
|
|
5
5
|
|
|
6
|
+
const DEFAULT_CAMERA_SCALE = Object.freeze( new Vector3( 1, 1, 1 ) );
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Manages camera creation, switching, auto-focus, and AF point placement.
|
|
8
10
|
*
|
|
@@ -83,6 +85,7 @@ export class CameraManager extends EventDispatcher {
|
|
|
83
85
|
const cam = new PerspectiveCamera( src.fov, src.aspect, src.near, src.far );
|
|
84
86
|
cam.position.copy( src.position );
|
|
85
87
|
cam.quaternion.copy( src.quaternion );
|
|
88
|
+
cam.scale.copy( src.scale );
|
|
86
89
|
cam.updateMatrixWorld( true );
|
|
87
90
|
|
|
88
91
|
cam.userData.__rayzeeUserCamera = true;
|
|
@@ -259,6 +262,7 @@ export class CameraManager extends EventDispatcher {
|
|
|
259
262
|
this._defaultCameraState = {
|
|
260
263
|
position: this.camera.position.clone(),
|
|
261
264
|
quaternion: this.camera.quaternion.clone(),
|
|
265
|
+
scale: this.camera.scale.clone(),
|
|
262
266
|
fov: this.camera.fov,
|
|
263
267
|
near: this.camera.near,
|
|
264
268
|
far: this.camera.far,
|
|
@@ -275,6 +279,9 @@ export class CameraManager extends EventDispatcher {
|
|
|
275
279
|
const s = this._defaultCameraState;
|
|
276
280
|
this.camera.position.copy( s.position );
|
|
277
281
|
this.camera.quaternion.copy( s.quaternion );
|
|
282
|
+
// Clears a mirror picked up from an imported camera; older saved states
|
|
283
|
+
// predate the field and are unmirrored by definition.
|
|
284
|
+
this.camera.scale.copy( s.scale ?? DEFAULT_CAMERA_SCALE );
|
|
278
285
|
this.camera.fov = s.fov;
|
|
279
286
|
this.camera.near = s.near;
|
|
280
287
|
this.camera.far = s.far;
|
|
@@ -294,6 +301,10 @@ export class CameraManager extends EventDispatcher {
|
|
|
294
301
|
|
|
295
302
|
this.camera.position.copy( sourceCamera.position );
|
|
296
303
|
this.camera.quaternion.copy( sourceCamera.quaternion );
|
|
304
|
+
// An imported camera can carry a mirror (a negative axis scale) that no
|
|
305
|
+
// quaternion can express — a pbrt scene's `Scale -1 1 1`, for one. Copying
|
|
306
|
+
// pose alone would silently un-mirror the view.
|
|
307
|
+
this.camera.scale.copy( sourceCamera.scale );
|
|
297
308
|
this.camera.fov = sourceCamera.fov;
|
|
298
309
|
this.camera.near = sourceCamera.near;
|
|
299
310
|
this.camera.far = sourceCamera.far;
|