rayzee 11.0.0 → 11.1.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 +1 -1
- package/dist/{ArchiveImporter-BRQvLggb.js → ArchiveImporter-CFcggGqi.js} +1525 -680
- package/dist/BidirectionalIntegrator-DIPN8lJW.js +946 -0
- package/dist/{NeuralRetouch-BCXylThH.js → NeuralRetouch-Doo3a4vY.js} +1 -1
- package/dist/{NeuralSuperRes-sZAeS142.js → NeuralSuperRes-B-rNIU_R.js} +1 -1
- package/dist/{PathTracer-CQPW06Mm.js → PathTracer-D_LmDeLh.js} +79 -74
- package/dist/{PhysicalSky-BvqCw_dy.js → PhysicalSky-DYffGJ3O.js} +1 -1
- package/dist/{RayzeeRenderer-COpwotpU.js → RayzeeRenderer-DWx7D714.js} +3 -3
- package/dist/addons/archives.es.js +1 -1
- package/dist/addons/bidirectional.es.js +1 -1
- package/dist/addons/physical-sky.es.js +1 -1
- package/dist/{archiveFormats-BZw7TiDp.js → archiveFormats-tnAoUL1j.js} +28 -9
- package/dist/rayzee-core.es.js +4 -4
- package/dist/rayzee.es.js +12 -12
- package/dist/rayzee.umd.js +43 -43
- package/dist/{sceneCachePolicy-BSSsKY98.js → sceneCachePolicy-Bk8sI5PZ.js} +1 -1
- package/package.json +1 -1
- package/src/PathTracerApp.js +1 -0
- package/src/Processor/ArchiveCache.js +19 -6
- package/src/Processor/ArchiveImporter.js +9 -3
- package/src/Processor/ArchiveReader.js +22 -4
- package/src/Processor/PBRT/PBRTMaterials.js +132 -8
- package/src/Processor/PBRT/PBRTParser.js +20 -3
- package/src/Processor/PBRT/PBRTScattering.js +93 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +513 -49
- package/src/Processor/PBRT/PBRTTextureBake.js +32 -7
- package/src/Processor/SceneMetadata.js +30 -5
- package/src/RayzeeRenderer.js +3 -2
- package/src/Stages/PathTracer.js +3 -5
- package/src/TSL/Bidirectional.js +6 -2
- package/src/TSL/ConnectKernel.js +31 -11
- package/src/TSL/LightSplatKernel.js +22 -7
- package/src/TSL/MergeKernel.js +13 -7
- package/src/TSL/ShadeKernel.js +51 -30
- package/src/integrators/BidirectionalIntegrator.js +6 -1
- package/dist/BidirectionalIntegrator-9lZQxFVH.js +0 -934
package/package.json
CHANGED
package/src/PathTracerApp.js
CHANGED
|
@@ -16,30 +16,43 @@ export async function archiveKey( kind, file ) {
|
|
|
16
16
|
/**
|
|
17
17
|
* Decompresses a .tar.gz into storage once, indexing it on the way, and hands back the unpacked
|
|
18
18
|
* .tar as a seekable File. A second call for the same file skips the work.
|
|
19
|
+
*
|
|
20
|
+
* With `filter` (and `part`, naming what it keeps) only the entries it keeps are written, end to end, and only they
|
|
21
|
+
* get offsets in the listing: a 31 GB scene archive need not fit to load the part of it chosen. An unpack of the whole
|
|
22
|
+
* archive serves any part.
|
|
19
23
|
* @returns {Promise<?{file: File, index: {v:number, listing:Array}, release: function(): void, cached: boolean}>}
|
|
20
24
|
* null when storage cannot take it — the caller reads the archive the old way.
|
|
21
25
|
*/
|
|
22
|
-
export async function unpackTarGz( file, { storage, label = file.name, onProgress = null } ) {
|
|
26
|
+
export async function unpackTarGz( file, { storage, label = file.name, onProgress = null, filter = null, part = null } ) {
|
|
23
27
|
|
|
24
28
|
if ( ! storage || typeof DecompressionStream === 'undefined' ) return null;
|
|
25
29
|
|
|
26
30
|
const area = storage.area( ENGINE_AREAS.ARCHIVES );
|
|
27
|
-
const
|
|
31
|
+
const whole = await archiveKey( 'gunzip', file );
|
|
32
|
+
let key = whole;
|
|
28
33
|
|
|
29
|
-
let entry = await area.open(
|
|
34
|
+
let entry = await area.open( whole );
|
|
35
|
+
if ( ! entry && filter ) entry = await area.open( key = `${whole}|${part}` );
|
|
30
36
|
const cached = !! entry;
|
|
31
37
|
|
|
32
38
|
if ( ! entry ) {
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
// A part's size is known only once its headers are read: a write past the quota falls back below.
|
|
41
|
+
const writer = await area.create( key, filter ? { label: `${label} (${part})` } : { label, expectedBytes: file.size * GUESSED_EXPANSION } );
|
|
35
42
|
if ( ! writer ) return null;
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
let out = null;
|
|
45
|
+
const indexer = createTarIndexer( filter ? { filter, sink: ( bytes ) => out.enqueue( bytes ) } : {} );
|
|
38
46
|
const tap = new TransformStream( {
|
|
47
|
+
start( controller ) {
|
|
48
|
+
|
|
49
|
+
out = controller;
|
|
50
|
+
|
|
51
|
+
},
|
|
39
52
|
transform( chunk, controller ) {
|
|
40
53
|
|
|
41
54
|
indexer.push( chunk );
|
|
42
|
-
controller.enqueue( chunk );
|
|
55
|
+
if ( ! filter ) controller.enqueue( chunk );
|
|
43
56
|
|
|
44
57
|
},
|
|
45
58
|
} );
|
|
@@ -129,13 +129,15 @@ export class ArchiveImporter {
|
|
|
129
129
|
* A .tar.gz unpacked to storage, or null to read it the old way (no storage, or no room).
|
|
130
130
|
* @private
|
|
131
131
|
*/
|
|
132
|
-
async _unpackGzip( file, filename ) {
|
|
132
|
+
async _unpackGzip( file, filename, element = null ) {
|
|
133
133
|
|
|
134
|
+
const chosen = ( Array.isArray( element ) ? element : [ element ] ).filter( Boolean );
|
|
134
135
|
try {
|
|
135
136
|
|
|
136
137
|
return await unpackTarGz( file, {
|
|
137
138
|
storage: this.loader.storage,
|
|
138
139
|
label: filename,
|
|
140
|
+
...( chosen.length ? { filter: elementFilter( chosen ), part: [ ...chosen ].sort().join( ',' ) } : {} ),
|
|
139
141
|
onProgress: bytes => updateLoading( {
|
|
140
142
|
isLoading: true, status: `Unpacking archive… ${( bytes / 1e9 ).toFixed( 1 )} GB`, progress: 4
|
|
141
143
|
} ),
|
|
@@ -189,7 +191,7 @@ export class ArchiveImporter {
|
|
|
189
191
|
|
|
190
192
|
if ( kind === 'gzip' ) {
|
|
191
193
|
|
|
192
|
-
const unpacked = await this._unpackGzip( file, filename );
|
|
194
|
+
const unpacked = await this._unpackGzip( file, filename, element );
|
|
193
195
|
if ( unpacked ) {
|
|
194
196
|
|
|
195
197
|
try {
|
|
@@ -337,6 +339,7 @@ export class ArchiveImporter {
|
|
|
337
339
|
meshCount: built.meshCount, entryPath: built.entryPath, candidates: built.candidates, frames: built.frames,
|
|
338
340
|
warnings: built.warnings, triangleCount: built.triangleCount, placementCount: built.placementCount,
|
|
339
341
|
mergedShapes: built.mergedShapes, skippedForBudget: built.skippedForBudget, droppedNoTemplate: built.droppedNoTemplate,
|
|
342
|
+
render: built.render ?? null,
|
|
340
343
|
},
|
|
341
344
|
} );
|
|
342
345
|
|
|
@@ -680,7 +683,10 @@ export class ArchiveImporter {
|
|
|
680
683
|
// The light's own orientation and `scale` are already baked into the texture, so the
|
|
681
684
|
// scene is only correct at rotation 0 / intensity 1 — pinned, whatever the host's defaults.
|
|
682
685
|
// Without an infinite light pbrt has no environment at all.
|
|
683
|
-
this.loader.sceneMetadata = {
|
|
686
|
+
this.loader.sceneMetadata = {
|
|
687
|
+
environment: environment?.texture ? { rotation: 0, intensity: 1 } : { enabled: false },
|
|
688
|
+
...( built.render ? { render: { ...built.render } } : {} ),
|
|
689
|
+
};
|
|
684
690
|
|
|
685
691
|
updateLoading( { isLoading: true, status: 'Processing PBRT geometry...', progress: 10 } );
|
|
686
692
|
await this.loader.onModelLoad( this.loader.targetModel );
|
|
@@ -135,9 +135,12 @@ function isZeroBlock( h ) {
|
|
|
135
135
|
*/
|
|
136
136
|
class TarStream {
|
|
137
137
|
|
|
138
|
-
constructor( { filter = null, retain = null, byteBudget = DEFAULT_BYTE_BUDGET, onEntry = null } = {} ) {
|
|
138
|
+
constructor( { filter = null, retain = null, byteBudget = DEFAULT_BYTE_BUDGET, onEntry = null, sink = null } = {} ) {
|
|
139
139
|
|
|
140
140
|
this.filter = filter;
|
|
141
|
+
// Passed each indexed entry's bytes in order: the listing then records offsets into what the sink wrote.
|
|
142
|
+
this.sink = sink;
|
|
143
|
+
this._sunk = 0;
|
|
141
144
|
// Entries that pass `filter` but fail `retain` are indexed, not copied: the listing
|
|
142
145
|
// records where their bytes live so a caller with a seekable source can read them later.
|
|
143
146
|
// An uncompressed tar over a File is seekable, which is how a 7 GB archive loads without
|
|
@@ -190,6 +193,14 @@ class TarStream {
|
|
|
190
193
|
} else {
|
|
191
194
|
|
|
192
195
|
const n = Math.min( this._remaining, chunk.length - i );
|
|
196
|
+
if ( this.sink && this._indexed && this._sinkLeft > 0 ) {
|
|
197
|
+
|
|
198
|
+
const take = Math.min( n, this._sinkLeft );
|
|
199
|
+
this.sink( chunk.subarray( i, i + take ) );
|
|
200
|
+
this._sinkLeft -= take;
|
|
201
|
+
|
|
202
|
+
}
|
|
203
|
+
|
|
193
204
|
if ( this._dst && this._copied < this._size ) {
|
|
194
205
|
|
|
195
206
|
const take = Math.min( n, this._size - this._copied );
|
|
@@ -267,6 +278,13 @@ class TarStream {
|
|
|
267
278
|
|
|
268
279
|
this.listing.push( { path, size } );
|
|
269
280
|
this._indexed = this.filter ? this.filter( path, size ) : true;
|
|
281
|
+
if ( this.sink && this._indexed ) {
|
|
282
|
+
|
|
283
|
+
this._sinkStart = this._sunk;
|
|
284
|
+
this._sinkLeft = size;
|
|
285
|
+
this._sunk += size;
|
|
286
|
+
|
|
287
|
+
}
|
|
270
288
|
|
|
271
289
|
if ( this._indexed && ! ( this.retain && ! this.retain( path, size ) ) ) {
|
|
272
290
|
|
|
@@ -313,7 +331,7 @@ class TarStream {
|
|
|
313
331
|
|
|
314
332
|
if ( this._indexed && entry ) {
|
|
315
333
|
|
|
316
|
-
entry.offset = this._bodyStart;
|
|
334
|
+
entry.offset = this.sink ? this._sinkStart : this._bodyStart;
|
|
317
335
|
this._indexed = false;
|
|
318
336
|
|
|
319
337
|
}
|
|
@@ -503,9 +521,9 @@ export async function indexTarHeaders( source, { onProgress = null } = {} ) {
|
|
|
503
521
|
* Indexes a tar stream as it goes by — for writing a decompressing archive to disk and knowing
|
|
504
522
|
* where every entry landed without a second pass.
|
|
505
523
|
*/
|
|
506
|
-
export function createTarIndexer() {
|
|
524
|
+
export function createTarIndexer( { filter = null, sink = null } = {} ) {
|
|
507
525
|
|
|
508
|
-
const tar = new TarStream( { retain: () => false } );
|
|
526
|
+
const tar = new TarStream( { retain: () => false, filter, sink } );
|
|
509
527
|
return {
|
|
510
528
|
push: chunk => tar.push( chunk ),
|
|
511
529
|
finish: () => {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { MeshPhysicalMaterial, Color, DoubleSide } from 'three';
|
|
12
|
+
import { namedScattering, dipoleReflectance, albedoForReflectance } from './PBRTScattering.js';
|
|
12
13
|
|
|
13
14
|
// Normal-incidence reflectance approximations for pbrt's named conductor spectra.
|
|
14
15
|
const METAL_ALBEDO = {
|
|
@@ -168,10 +169,17 @@ async function floatOrMean( params, name, ctx ) {
|
|
|
168
169
|
* α = √roughness (`remaproughness`, the default) or α = roughness; the engine's GGX takes α = roughness². Passing
|
|
169
170
|
* pbrt's number through made `remaproughness false` 0.1 an α of 0.01, a near mirror. `dflt` is the engine's.
|
|
170
171
|
*/
|
|
171
|
-
|
|
172
|
+
const remapsRoughness = ( params ) => {
|
|
172
173
|
|
|
173
174
|
const remap = params.remaproughness?.value?.[ 0 ];
|
|
174
|
-
|
|
175
|
+
return remap !== false && remap !== 'false';
|
|
176
|
+
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
async function resolveRoughness( params, dflt, ctx, prefix = '' ) {
|
|
180
|
+
|
|
181
|
+
const remap = remapsRoughness( params );
|
|
182
|
+
const alpha = ( r ) => ( remap ? Math.sqrt( Math.max( r, 0 ) ) : r );
|
|
175
183
|
let a = null;
|
|
176
184
|
const r = await floatOrMean( params, `${prefix}roughness`, ctx );
|
|
177
185
|
if ( r !== null ) a = alpha( r );
|
|
@@ -187,6 +195,116 @@ async function resolveRoughness( params, dflt, ctx, prefix = '' ) {
|
|
|
187
195
|
|
|
188
196
|
}
|
|
189
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Sets `target` (`roughness` or `clearcoatRoughness`) as resolveRoughness does, except that a textured roughness becomes
|
|
200
|
+
* a roughness map with the remap baked in (`ctx.bakeFloatMap`), the value 1 multiplying it.
|
|
201
|
+
*/
|
|
202
|
+
async function applyRoughness( mat, target, params, dflt, ctx, prefix = '' ) {
|
|
203
|
+
|
|
204
|
+
const single = params[ `${prefix}roughness` ];
|
|
205
|
+
const keys = single ? [ `${prefix}roughness` ] : [ `${prefix}uroughness`, `${prefix}vroughness` ].filter( ( key ) => params[ key ] );
|
|
206
|
+
if ( ctx.bakeFloatMap && keys.some( ( key ) => params[ key ].type === 'texture' ) ) {
|
|
207
|
+
|
|
208
|
+
const remap = remapsRoughness( params );
|
|
209
|
+
const alpha = ( r ) => ( remap ? Math.sqrt( Math.max( r, 0 ) ) : Math.max( r, 0 ) );
|
|
210
|
+
const map = await ctx.bakeFloatMap( params, keys, ( values ) => Math.sqrt( values.reduce( ( sum, r ) => sum + alpha( r ), 0 ) / values.length ) );
|
|
211
|
+
if ( map ) {
|
|
212
|
+
|
|
213
|
+
mat[ `${target}Map` ] = map;
|
|
214
|
+
mat[ target ] = 1;
|
|
215
|
+
return;
|
|
216
|
+
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
mat[ target ] = await resolveRoughness( params, dflt, ctx, prefix );
|
|
222
|
+
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// A spectrum parameter as one RGB; a texture counts as its mean (`ctx.textureMeanRGB`).
|
|
226
|
+
async function spectrumMean( params, key, ctx, dflt ) {
|
|
227
|
+
|
|
228
|
+
const p = params[ key ];
|
|
229
|
+
if ( p?.type === 'texture' ) return ( await ctx.textureMeanRGB?.( p.value[ 0 ] ) ) ?? dflt;
|
|
230
|
+
return ( await resolveSpectrum( params, key, ctx, dflt ) ).rgb ?? dflt;
|
|
231
|
+
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// pbrt's default subsurface medium (materials.cpp), mm⁻¹.
|
|
235
|
+
const SUBSURFACE_SIGMA_A = [ 0.0011, 0.0024, 0.014 ];
|
|
236
|
+
const SUBSURFACE_SIGMA_S = [ 2.55, 3.21, 3.77 ];
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* pbrt's `subsurface` as the engine's random walk under a dielectric boundary of `eta`: σt = σa + σs (times `scale`),
|
|
240
|
+
* single-scattering albedo σs / σt, mean free path 1 / σt. The coefficients come as pbrt takes them — a named medium
|
|
241
|
+
* (g 0), `sigma_a` and `sigma_s`, or a `reflectance` and `mfp` (scaled) inverted through the dipole — and default to
|
|
242
|
+
* pbrt's. The base colour is the reflectance the dipole gives, for what reads it as an albedo.
|
|
243
|
+
*/
|
|
244
|
+
async function applySubsurface( mat, params, ctx ) {
|
|
245
|
+
|
|
246
|
+
const eta = pFloat( params, 'eta', 1.33 );
|
|
247
|
+
const scale = pFloat( params, 'scale', 1 );
|
|
248
|
+
const scaled = ( rgb ) => rgb.map( ( v ) => Math.max( 0, v * scale ) );
|
|
249
|
+
let g = pFloat( params, 'g', 0 );
|
|
250
|
+
let sigmaA, sigmaS;
|
|
251
|
+
|
|
252
|
+
const name = pString( params, 'name', '' );
|
|
253
|
+
const named = name ? namedScattering( name ) : null;
|
|
254
|
+
if ( name && ! named ) ctx.warn( `subsurface: no medium named "${name}" — using pbrt's default` );
|
|
255
|
+
|
|
256
|
+
if ( named ) {
|
|
257
|
+
|
|
258
|
+
sigmaA = scaled( named.sigmaA );
|
|
259
|
+
sigmaS = scaled( named.sigmaS );
|
|
260
|
+
g = 0;
|
|
261
|
+
|
|
262
|
+
} else if ( params.sigma_a && params.sigma_s ) {
|
|
263
|
+
|
|
264
|
+
sigmaA = scaled( await spectrumMean( params, 'sigma_a', ctx, SUBSURFACE_SIGMA_A ) );
|
|
265
|
+
sigmaS = scaled( await spectrumMean( params, 'sigma_s', ctx, SUBSURFACE_SIGMA_S ) );
|
|
266
|
+
|
|
267
|
+
} else if ( params.reflectance && ! name ) {
|
|
268
|
+
|
|
269
|
+
const reflectance = await spectrumMean( params, 'reflectance', ctx, [ 0.5, 0.5, 0.5 ] );
|
|
270
|
+
const mfp = scaled( params.mfp ? await spectrumMean( params, 'mfp', ctx, [ 1, 1, 1 ] ) : [ 1, 1, 1 ] );
|
|
271
|
+
sigmaA = [];
|
|
272
|
+
sigmaS = [];
|
|
273
|
+
for ( let c = 0; c < 3; c ++ ) {
|
|
274
|
+
|
|
275
|
+
const sigmaT = 1 / Math.max( mfp[ c ], 1e-9 );
|
|
276
|
+
// The reduced albedo σ′s / ( σa + σ′s ), σ′s = ( 1 − g ) σs, back to σs at this σt.
|
|
277
|
+
const reduced = albedoForReflectance( reflectance[ c ], eta );
|
|
278
|
+
sigmaS[ c ] = reduced * sigmaT / ( 1 - g + reduced * g );
|
|
279
|
+
sigmaA[ c ] = sigmaT - sigmaS[ c ];
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
} else {
|
|
284
|
+
|
|
285
|
+
sigmaA = scaled( SUBSURFACE_SIGMA_A );
|
|
286
|
+
sigmaS = scaled( SUBSURFACE_SIGMA_S );
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const sigmaT = sigmaA.map( ( a, c ) => a + sigmaS[ c ] );
|
|
291
|
+
mat.subsurface = 1;
|
|
292
|
+
mat.subsurfaceColor = new Color( ...sigmaT.map( ( t, c ) => ( t > 0 ? sigmaS[ c ] / t : 0 ) ) );
|
|
293
|
+
mat.subsurfaceRadius = sigmaT.map( ( t ) => ( t > 0 ? 1 / t : 1e6 ) );
|
|
294
|
+
mat.subsurfaceRadiusScale = 1;
|
|
295
|
+
mat.subsurfaceAnisotropy = Math.max( - 0.99, Math.min( 0.99, g ) );
|
|
296
|
+
mat.ior = eta;
|
|
297
|
+
mat.metalness = 0;
|
|
298
|
+
mat.color.setRGB( ...sigmaT.map( ( t, c ) => {
|
|
299
|
+
|
|
300
|
+
const reducedS = ( 1 - g ) * sigmaS[ c ];
|
|
301
|
+
return t > 0 ? dipoleReflectance( reducedS / ( sigmaA[ c ] + reducedS ), eta ) : 0;
|
|
302
|
+
|
|
303
|
+
} ) );
|
|
304
|
+
await applyRoughness( mat, 'roughness', params, 0, ctx );
|
|
305
|
+
|
|
306
|
+
}
|
|
307
|
+
|
|
190
308
|
// A conductor's colour: `reflectance`, else normal-incidence Fresnel of `eta`/`k` (under `prefix` for a coated
|
|
191
309
|
// conductor). pbrt's default is copper.
|
|
192
310
|
async function applyConductorColor( mat, params, ctx, prefix, applyAlbedo, setColor ) {
|
|
@@ -272,7 +390,7 @@ export async function buildMaterial( def, ctx ) {
|
|
|
272
390
|
case 'metal': {
|
|
273
391
|
|
|
274
392
|
await applyConductorColor( mat, params, ctx, '', applyAlbedo, setColor );
|
|
275
|
-
|
|
393
|
+
await applyRoughness( mat, 'roughness', params, 0.1, ctx );
|
|
276
394
|
break;
|
|
277
395
|
|
|
278
396
|
}
|
|
@@ -281,9 +399,9 @@ export async function buildMaterial( def, ctx ) {
|
|
|
281
399
|
|
|
282
400
|
// A conductor under a dielectric coat; pbrt's roughnesses default to 0 for both.
|
|
283
401
|
await applyConductorColor( mat, params, ctx, 'conductor.', applyAlbedo, setColor );
|
|
284
|
-
|
|
402
|
+
await applyRoughness( mat, 'roughness', params, 0, ctx, 'conductor.' );
|
|
285
403
|
mat.clearcoat = 1;
|
|
286
|
-
|
|
404
|
+
await applyRoughness( mat, 'clearcoatRoughness', params, 0, ctx, 'interface.' );
|
|
287
405
|
break;
|
|
288
406
|
|
|
289
407
|
}
|
|
@@ -295,7 +413,7 @@ export async function buildMaterial( def, ctx ) {
|
|
|
295
413
|
mat.metalness = 0;
|
|
296
414
|
mat.color.setRGB( 1, 1, 1 );
|
|
297
415
|
mat.ior = pFloat( params, 'eta', 1.5 );
|
|
298
|
-
|
|
416
|
+
await applyRoughness( mat, 'roughness', params, 0, ctx );
|
|
299
417
|
mat.thickness = type === 'thindielectric' ? 0 : pFloat( params, 'thickness', 0 );
|
|
300
418
|
break;
|
|
301
419
|
|
|
@@ -307,7 +425,7 @@ export async function buildMaterial( def, ctx ) {
|
|
|
307
425
|
mat.roughness = 0.6;
|
|
308
426
|
mat.metalness = 0;
|
|
309
427
|
mat.clearcoat = 1;
|
|
310
|
-
|
|
428
|
+
await applyRoughness( mat, 'clearcoatRoughness', params, 0, ctx );
|
|
311
429
|
break;
|
|
312
430
|
|
|
313
431
|
}
|
|
@@ -407,7 +525,13 @@ export async function buildMaterial( def, ctx ) {
|
|
|
407
525
|
|
|
408
526
|
}
|
|
409
527
|
|
|
410
|
-
case 'subsurface':
|
|
528
|
+
case 'subsurface': {
|
|
529
|
+
|
|
530
|
+
await applySubsurface( mat, params, ctx );
|
|
531
|
+
break;
|
|
532
|
+
|
|
533
|
+
}
|
|
534
|
+
|
|
411
535
|
case 'hair':
|
|
412
536
|
case 'measured': {
|
|
413
537
|
|
|
@@ -603,15 +603,33 @@ export class PBRTParser {
|
|
|
603
603
|
this.ir.film = {
|
|
604
604
|
xresolution: this._num( params.xresolution, 1280 ),
|
|
605
605
|
yresolution: this._num( params.yresolution, 720 ),
|
|
606
|
+
resolutionGiven: Boolean( params.xresolution || params.yresolution ),
|
|
606
607
|
filename: this._str( params.filename, null )
|
|
607
608
|
};
|
|
608
609
|
break;
|
|
609
610
|
|
|
610
611
|
}
|
|
611
612
|
|
|
613
|
+
// What the scene asks the renderer for; reported, not applied (PBRTSceneBuilder renderRequest).
|
|
614
|
+
case 'Integrator': {
|
|
615
|
+
|
|
616
|
+
this._expectString( 'Integrator type' );
|
|
617
|
+
const params = this._parseParams();
|
|
618
|
+
if ( params.maxdepth ) this.ir.integrator = { maxdepth: this._num( params.maxdepth, 5 ) };
|
|
619
|
+
break;
|
|
620
|
+
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
case 'Sampler': {
|
|
624
|
+
|
|
625
|
+
this._expectString( 'Sampler type' );
|
|
626
|
+
const params = this._parseParams();
|
|
627
|
+
if ( params.pixelsamples ) this.ir.sampler = { pixelsamples: this._num( params.pixelsamples, 16 ) };
|
|
628
|
+
break;
|
|
629
|
+
|
|
630
|
+
}
|
|
631
|
+
|
|
612
632
|
// Consumed for completeness; not used by the engine.
|
|
613
|
-
case 'Integrator':
|
|
614
|
-
case 'Sampler':
|
|
615
633
|
case 'PixelFilter':
|
|
616
634
|
case 'Filter':
|
|
617
635
|
case 'Accelerator':
|
|
@@ -759,7 +777,6 @@ export class PBRTParser {
|
|
|
759
777
|
// pbrt: a shape of alpha 0 is never hit, and an area light on it emits nothing.
|
|
760
778
|
const alpha = params.alpha;
|
|
761
779
|
if ( alpha?.type === 'float' && alpha.value[ 0 ] <= 0 ) break;
|
|
762
|
-
if ( alpha && ! ( alpha.type === 'float' && alpha.value[ 0 ] >= 1 ) ) this._warnOnce( 'alpha', 'shape "alpha" between 0 and 1, or as a texture, is not supported — drawn opaque' );
|
|
763
780
|
const shape = {
|
|
764
781
|
type,
|
|
765
782
|
params,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scattering coefficients for pbrt's `subsurface` material: its named media and the inversion from a diffuse
|
|
3
|
+
* reflectance to the albedo that produces it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// pbrt-v4's table (media.cpp): [ σ′s, σa ] in mm⁻¹, from Jensen et al. 2001 and Narasimhan et al. 2006.
|
|
7
|
+
const MEASURED = {
|
|
8
|
+
'Apple': [[ 2.29, 2.39, 1.97 ], [ 0.0030, 0.0034, 0.046 ]],
|
|
9
|
+
'Chicken1': [[ 0.15, 0.21, 0.38 ], [ 0.015, 0.077, 0.19 ]],
|
|
10
|
+
'Chicken2': [[ 0.19, 0.25, 0.32 ], [ 0.018, 0.088, 0.20 ]],
|
|
11
|
+
'Cream': [[ 7.38, 5.47, 3.15 ], [ 0.0002, 0.0028, 0.0163 ]],
|
|
12
|
+
'Ketchup': [[ 0.18, 0.07, 0.03 ], [ 0.061, 0.97, 1.45 ]],
|
|
13
|
+
'Marble': [[ 2.19, 2.62, 3.00 ], [ 0.0021, 0.0041, 0.0071 ]],
|
|
14
|
+
'Potato': [[ 0.68, 0.70, 0.55 ], [ 0.0024, 0.0090, 0.12 ]],
|
|
15
|
+
'Skimmilk': [[ 0.70, 1.22, 1.90 ], [ 0.0014, 0.0025, 0.0142 ]],
|
|
16
|
+
'Skin1': [[ 0.74, 0.88, 1.01 ], [ 0.032, 0.17, 0.48 ]],
|
|
17
|
+
'Skin2': [[ 1.09, 1.59, 1.79 ], [ 0.013, 0.070, 0.145 ]],
|
|
18
|
+
'Spectralon': [[ 11.6, 20.4, 14.9 ], [ 0.00, 0.00, 0.00 ]],
|
|
19
|
+
'Wholemilk': [[ 2.55, 3.21, 3.77 ], [ 0.0011, 0.0024, 0.014 ]],
|
|
20
|
+
'Lowfat Milk': [[ 0.89187, 1.5136, 2.532 ], [ 0.002875, 0.00575, 0.0115 ]],
|
|
21
|
+
'Reduced Milk': [[ 2.4858, 3.1669, 4.5214 ], [ 0.0025556, 0.0051111, 0.012778 ]],
|
|
22
|
+
'Regular Milk': [[ 4.5513, 5.8294, 7.136 ], [ 0.0015333, 0.0046, 0.019933 ]],
|
|
23
|
+
'Espresso': [[ 0.72378, 0.84557, 1.0247 ], [ 4.7984, 6.5751, 8.8493 ]],
|
|
24
|
+
'Mint Mocha Coffee': [[ 0.31602, 0.38538, 0.48131 ], [ 3.772, 5.8228, 7.82 ]],
|
|
25
|
+
'Lowfat Soy Milk': [[ 0.30576, 0.34233, 0.61664 ], [ 0.0014375, 0.0071875, 0.035937 ]],
|
|
26
|
+
'Regular Soy Milk': [[ 0.59223, 0.73866, 1.4693 ], [ 0.0019167, 0.0095833, 0.065167 ]],
|
|
27
|
+
'Lowfat Chocolate Milk': [[ 0.64925, 0.83916, 1.1057 ], [ 0.0115, 0.0368, 0.1564 ]],
|
|
28
|
+
'Regular Chocolate Milk': [[ 1.4585, 2.1289, 2.9527 ], [ 0.010063, 0.043125, 0.14375 ]],
|
|
29
|
+
'Coke': [[ 8.9053e-05, 8.372e-05, 0 ], [ 0.10014, 0.16503, 0.2468 ]],
|
|
30
|
+
'Pepsi': [[ 6.1697e-05, 4.2564e-05, 0 ], [ 0.091641, 0.14158, 0.20729 ]],
|
|
31
|
+
'Sprite': [[ 6.0306e-06, 6.4139e-06, 6.5504e-06 ], [ 0.001886, 0.0018308, 0.0020025 ]],
|
|
32
|
+
'Gatorade': [[ 0.0024574, 0.003007, 0.0037325 ], [ 0.024794, 0.019289, 0.008878 ]],
|
|
33
|
+
'Chardonnay': [[ 1.7982e-05, 1.3758e-05, 1.2023e-05 ], [ 0.010782, 0.011855, 0.023997 ]],
|
|
34
|
+
'White Zinfandel': [[ 1.7501e-05, 1.9069e-05, 1.288e-05 ], [ 0.012072, 0.016184, 0.019843 ]],
|
|
35
|
+
'Merlot': [[ 2.1129e-05, 0, 0 ], [ 0.11632, 0.25191, 0.29434 ]],
|
|
36
|
+
'Budweiser Beer': [[ 2.4356e-05, 2.4079e-05, 1.0564e-05 ], [ 0.011492, 0.024911, 0.057786 ]],
|
|
37
|
+
'Coors Light Beer': [[ 5.0922e-05, 4.301e-05, 0 ], [ 0.006164, 0.013984, 0.034983 ]],
|
|
38
|
+
'Clorox': [[ 0.0024035, 0.0031373, 0.003991 ], [ 0.0033542, 0.014892, 0.026297 ]],
|
|
39
|
+
'Apple Juice': [[ 0.00013612, 0.00015836, 0.000227 ], [ 0.012957, 0.023741, 0.052184 ]],
|
|
40
|
+
'Cranberry Juice': [[ 0.00010402, 0.00011646, 7.8139e-05 ], [ 0.039437, 0.094223, 0.12426 ]],
|
|
41
|
+
'Grape Juice': [[ 5.382e-05, 0, 0 ], [ 0.10404, 0.23958, 0.29325 ]],
|
|
42
|
+
'Ruby Grapefruit Juice': [[ 0.011002, 0.010927, 0.011036 ], [ 0.085867, 0.18314, 0.25262 ]],
|
|
43
|
+
'White Grapefruit Juice': [[ 0.22826, 0.23998, 0.32748 ], [ 0.0138, 0.018831, 0.056781 ]],
|
|
44
|
+
'Shampoo': [[ 0.0007176, 0.0008303, 0.0009016 ], [ 0.014107, 0.045693, 0.061717 ]],
|
|
45
|
+
'Strawberry Shampoo': [[ 0.00015671, 0.00015947, 1.518e-05 ], [ 0.01449, 0.05796, 0.075823 ]],
|
|
46
|
+
'Head & Shoulders Shampoo': [[ 0.023805, 0.028804, 0.034306 ], [ 0.084621, 0.15688, 0.20365 ]],
|
|
47
|
+
'Lemon Tea Powder': [[ 0.040224, 0.045264, 0.051081 ], [ 2.4288, 4.5757, 7.2127 ]],
|
|
48
|
+
'Orange Powder': [[ 0.00015617, 0.00017482, 0.0001762 ], [ 0.001449, 0.003441, 0.007863 ]],
|
|
49
|
+
'Pink Lemonade Powder': [[ 0.00012103, 0.00013073, 0.00012528 ], [ 0.001165, 0.002366, 0.003195 ]],
|
|
50
|
+
'Cappuccino Powder': [[ 1.8436, 2.5851, 2.1662 ], [ 35.844, 49.547, 61.084 ]],
|
|
51
|
+
'Salt Powder': [[ 0.027333, 0.032451, 0.031979 ], [ 0.28415, 0.3257, 0.34148 ]],
|
|
52
|
+
'Sugar Powder': [[ 0.00022272, 0.00025513, 0.000271 ], [ 0.012638, 0.031051, 0.050124 ]],
|
|
53
|
+
'Suisse Mocha Powder': [[ 2.7979, 3.5452, 4.3365 ], [ 17.502, 27.004, 35.433 ]],
|
|
54
|
+
'Pacific Ocean Surface Water': [[ 0.0001764, 0.00032095, 0.00019617 ], [ 0.031845, 0.031324, 0.030147 ]],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** A named medium's `{ sigmaS, sigmaA }` (σ′s with g = 0), or null. */
|
|
58
|
+
export function namedScattering( name ) {
|
|
59
|
+
|
|
60
|
+
const entry = MEASURED[ name ];
|
|
61
|
+
return entry ? { sigmaS: entry[ 0 ].slice(), sigmaA: entry[ 1 ].slice() } : null;
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Total diffuse reflectance of a half space of reduced albedo α′ and relative index η under the classical dipole
|
|
67
|
+
* (Jensen et al. 2001): (α′ / 2) (1 + e^(−4/3 A √(3(1 − α′)))) e^(−√(3(1 − α′))).
|
|
68
|
+
*/
|
|
69
|
+
export function dipoleReflectance( albedo, eta ) {
|
|
70
|
+
|
|
71
|
+
const fdr = - 1.440 / ( eta * eta ) + 0.710 / eta + 0.668 + 0.0636 * eta;
|
|
72
|
+
const A = ( 1 + fdr ) / ( 1 - fdr );
|
|
73
|
+
const s = Math.sqrt( 3 * ( 1 - albedo ) );
|
|
74
|
+
return albedo / 2 * ( 1 + Math.exp( - 4 / 3 * A * s ) ) * Math.exp( - s );
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The reduced albedo whose dipole reflectance is `reflectance` (pbrt inverts its own table the same way). */
|
|
79
|
+
export function albedoForReflectance( reflectance, eta ) {
|
|
80
|
+
|
|
81
|
+
const target = Math.min( Math.max( reflectance, 0 ), dipoleReflectance( 1, eta ) );
|
|
82
|
+
let lo = 0, hi = 1;
|
|
83
|
+
for ( let i = 0; i < 48; i ++ ) {
|
|
84
|
+
|
|
85
|
+
const mid = ( lo + hi ) / 2;
|
|
86
|
+
if ( dipoleReflectance( mid, eta ) < target ) lo = mid;
|
|
87
|
+
else hi = mid;
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return ( lo + hi ) / 2;
|
|
92
|
+
|
|
93
|
+
}
|