rayzee 6.3.0 → 6.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.
@@ -0,0 +1,179 @@
1
+ /**
2
+ * PBRT-v4 scene loader.
3
+ *
4
+ * Orchestrates: virtual filesystem (from a zip) → tokenize/parse the entry
5
+ * .pbrt (following Include/Import) → build a THREE scene graph. Geometry,
6
+ * image, and HDR decoding are injected by the host (AssetLoader owns the
7
+ * three/examples loaders) so this module stays dependency-light.
8
+ *
9
+ * Usage (from AssetLoader):
10
+ * const { group, environment, warnings } = await loadPBRTScene({
11
+ * vfs, entryPath, plyParser, imageFromBytes, envFromBytes
12
+ * });
13
+ * scene.environment = environment?.texture ?? scene.environment;
14
+ * await loadObject3D(group);
15
+ */
16
+
17
+ import { PBRTParser } from './PBRTParser.js';
18
+ import { PBRTSceneBuilder } from './PBRTSceneBuilder.js';
19
+
20
+ export { PBRTParser } from './PBRTParser.js';
21
+ export { PBRTSceneBuilder } from './PBRTSceneBuilder.js';
22
+ export { tokenize } from './PBRTTokenizer.js';
23
+
24
+ const decoder = new TextDecoder();
25
+
26
+ /** Normalize a path: forward slashes, collapse "./" and "../". */
27
+ function normalizePath( p ) {
28
+
29
+ const parts = p.replace( /\\/g, '/' ).split( '/' );
30
+ const out = [];
31
+ for ( const part of parts ) {
32
+
33
+ if ( part === '' || part === '.' ) continue;
34
+ if ( part === '..' ) out.pop();
35
+ else out.push( part );
36
+
37
+ }
38
+
39
+ return out.join( '/' );
40
+
41
+ }
42
+
43
+ /** Join a base directory with a relative path. */
44
+ function joinPath( dir, rel ) {
45
+
46
+ if ( ! dir ) return normalizePath( rel );
47
+ if ( rel.startsWith( '/' ) ) return normalizePath( rel );
48
+ return normalizePath( `${dir}/${rel}` );
49
+
50
+ }
51
+
52
+ /**
53
+ * Wraps the zip contents with tolerant, case-insensitive lookup that falls back
54
+ * to a basename match — pbrt scenes are inconsistent about path roots.
55
+ */
56
+ class VirtualFS {
57
+
58
+ constructor( entries ) {
59
+
60
+ // entries: { path: Uint8Array }
61
+ this.byPath = new Map(); // normalized lowercase -> { norm, bytes }
62
+ this.byBase = new Map(); // basename lowercase -> [ { norm, bytes } ] (insertion order)
63
+ for ( const key in entries ) {
64
+
65
+ const norm = normalizePath( key ).toLowerCase();
66
+ const rec = { norm, bytes: entries[ key ] };
67
+ this.byPath.set( norm, rec );
68
+ const base = norm.split( '/' ).pop();
69
+ const bucket = this.byBase.get( base );
70
+ if ( bucket ) bucket.push( rec );
71
+ else this.byBase.set( base, [ rec ] );
72
+
73
+ }
74
+
75
+ }
76
+
77
+ find( path ) {
78
+
79
+ const norm = normalizePath( path ).toLowerCase();
80
+ if ( this.byPath.has( norm ) ) return this.byPath.get( norm ).bytes;
81
+
82
+ // Resolve by basename (O(1)); among collisions prefer a path-suffix match,
83
+ // else fall back to the first entry with that name. pbrt scenes are
84
+ // inconsistent about path roots, so this tolerates relative/absolute drift.
85
+ const bucket = this.byBase.get( norm.split( '/' ).pop() );
86
+ if ( ! bucket ) return null;
87
+ const suffixHit = bucket.find( rec => rec.norm.endsWith( '/' + norm ) );
88
+ return ( suffixHit || bucket[ 0 ] ).bytes;
89
+
90
+ }
91
+
92
+ }
93
+
94
+ /** Pick the top-level .pbrt entry: shallowest path, preferring scene/main names. */
95
+ export function pickEntryPath( entries ) {
96
+
97
+ const pbrts = Object.keys( entries ).filter( k => k.toLowerCase().endsWith( '.pbrt' ) );
98
+ if ( pbrts.length === 0 ) return null;
99
+
100
+ const preferred = pbrts.filter( k => /(^|\/)(scene|main)\.pbrt$/i.test( k ) );
101
+ const pool = preferred.length ? preferred : pbrts;
102
+
103
+ // Shallowest (fewest path segments), then shortest name.
104
+ pool.sort( ( a, b ) => {
105
+
106
+ const da = a.split( '/' ).length, db = b.split( '/' ).length;
107
+ return da !== db ? da - db : a.length - b.length;
108
+
109
+ } );
110
+
111
+ return pool[ 0 ];
112
+
113
+ }
114
+
115
+ /**
116
+ * @param {object} args
117
+ * @param {Object<string,Uint8Array>} args.vfs - zip entries (path → bytes)
118
+ * @param {string} [args.entryPath] - top .pbrt; auto-detected if omitted
119
+ * @param {(buf:ArrayBuffer)=>import('three').BufferGeometry} args.plyParser
120
+ * @param {(bytes:Uint8Array, filename:string)=>Promise<import('three').Texture>} args.imageFromBytes
121
+ * @param {(bytes:Uint8Array, filename:string)=>Promise<import('three').Texture>} [args.envFromBytes]
122
+ * @param {boolean} [args.convertHandedness=true]
123
+ * @returns {Promise<{group, camera, environment, warnings, entryPath}>}
124
+ */
125
+ export async function loadPBRTScene( args ) {
126
+
127
+ const { vfs: rawEntries, plyParser, imageFromBytes, envFromBytes, convertHandedness } = args;
128
+ const vfs = new VirtualFS( rawEntries );
129
+
130
+ const entryPath = args.entryPath || pickEntryPath( rawEntries );
131
+ if ( ! entryPath ) throw new Error( 'PBRT loader: no .pbrt file found in archive' );
132
+
133
+ const entryBytes = vfs.find( entryPath );
134
+ if ( ! entryBytes ) throw new Error( `PBRT loader: entry "${entryPath}" not readable` );
135
+
136
+ const baseDir = entryPath.includes( '/' ) ? entryPath.slice( 0, entryPath.lastIndexOf( '/' ) ) : '';
137
+
138
+ // Parse (with Include resolution)
139
+ const parser = new PBRTParser( {
140
+ resolveInclude: ( path, currentDir ) => {
141
+
142
+ const bytes = vfs.find( joinPath( currentDir, path ) ) || vfs.find( path );
143
+ return bytes ? decoder.decode( bytes ) : null;
144
+
145
+ }
146
+ } );
147
+ const ir = parser.parse( decoder.decode( entryBytes ), baseDir );
148
+
149
+ // Build scene graph
150
+ const sliceBuf = ( bytes ) => bytes.buffer.slice( bytes.byteOffset, bytes.byteOffset + bytes.byteLength );
151
+ const builder = new PBRTSceneBuilder( {
152
+ convertHandedness,
153
+ resolvePLY: async ( filename ) => {
154
+
155
+ const bytes = vfs.find( filename );
156
+ if ( ! bytes ) return null;
157
+ return plyParser( sliceBuf( bytes ) );
158
+
159
+ },
160
+ resolveImage: async ( filename ) => {
161
+
162
+ const bytes = vfs.find( filename );
163
+ if ( ! bytes ) return null;
164
+ return imageFromBytes( bytes, filename );
165
+
166
+ },
167
+ resolveEnvironment: async ( filename ) => {
168
+
169
+ const bytes = vfs.find( filename );
170
+ if ( ! bytes ) return null;
171
+ return ( envFromBytes || imageFromBytes )( bytes, filename );
172
+
173
+ }
174
+ } );
175
+
176
+ const result = await builder.build( ir );
177
+ return { ...result, entryPath };
178
+
179
+ }
@@ -918,6 +918,18 @@ export const Trace = Fn( ( [
918
918
  const randomSample = getRandomSample( pixelCoord, rayIndex, bounceIndex, rngState, int( - 1 ), resolution, frame ).toVar();
919
919
 
920
920
  const V = rayDirection.negate().toVar();
921
+
922
+ // Two-sided shading: flip the shading normal into the viewer's hemisphere.
923
+ // This is the opaque reflection path (transmissive rays already Continue'd),
924
+ // so it never disturbs dielectric entering/exiting. No-op when N already
925
+ // faces V; rescues meshes with inward-facing normals (common in imported
926
+ // scenes, e.g. pbrt PLY assets) that would otherwise shade black.
927
+ If( dot( N, V ).lessThan( 0.0 ), () => {
928
+
929
+ N.assign( N.negate() );
930
+
931
+ } );
932
+
921
933
  material.sheenRoughness.assign( clamp( material.sheenRoughness, MIN_ROUGHNESS, MAX_ROUGHNESS ) );
922
934
 
923
935
  // Sync material classification cache up front — the materialCache, BRDF