rayzee 10.0.0 → 11.1.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/README.md +252 -44
- package/dist/ActiveColor-YvuQq44O.js +13 -0
- package/dist/ArchiveImporter-Hn35o6Z5.js +5145 -0
- package/dist/AssetConfig-DSEntE65.js +30 -0
- package/dist/BidirectionalIntegrator-CBw8hkeu.js +946 -0
- package/dist/BufferLayout-D_IBo2ci.js +150 -0
- package/dist/ColorManagement-oeEk7oSu.js +1488 -0
- package/dist/EngineEvents-Ci8dVWCP.js +73 -0
- package/dist/EngineIssues-CIZnXHsF.js +72 -0
- package/dist/FileFormats-sPGJeCRc.js +102 -0
- package/dist/Logger-dDDGj9Av.js +97 -0
- package/dist/{NeuralRetouch-CYgkQY8Z.js → NeuralRetouch-BzNpiVHZ.js} +8 -6
- package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-Bk7bHg2B.js} +55 -120
- package/dist/PathTracer-72XufKAr.js +29976 -0
- package/dist/PhysicalSky-BgjdV88k.js +1219 -0
- package/dist/RayzeeRenderer-1QRO97eq.js +3551 -0
- package/dist/SolarLimb-CPaeZVwz.js +721 -0
- package/dist/ToneMapGPU-CjU3osDd.js +177 -0
- package/dist/ViewTransforms-CYN7bq2w.js +255 -0
- package/dist/WorkingMatrix-C6y233hT.js +27 -0
- package/dist/ZipReader-gDSWr0I5.js +98 -0
- package/dist/addons/archives.es.js +3 -0
- package/dist/addons/bidirectional.es.js +2 -0
- package/dist/addons/color.es.js +3 -0
- package/dist/addons/formats.es.js +2 -0
- package/dist/addons/physical-sky.es.js +2 -0
- package/dist/addons/storage.es.js +3 -0
- package/dist/archiveFormats-tnAoUL1j.js +94 -0
- package/dist/identity-DrQsGT2o.js +39 -0
- package/dist/openStorage-DwcBRUj7.js +820 -0
- package/dist/rayzee-core.es.js +12 -0
- package/dist/rayzee.es.js +2551 -44024
- package/dist/rayzee.umd.js +528 -487
- package/dist/sceneCachePolicy-C67jbci-.js +443 -0
- package/dist/shared-BGBcSxBQ.js +17 -0
- package/package.json +23 -2
- package/src/Color/ActiveColor.js +34 -0
- package/src/Color/BasicColor.js +147 -0
- package/src/Color/ColorManagement.js +7 -31
- package/src/Color/ViewTransforms.js +5 -1
- package/src/EngineDefaults.js +62 -1029
- package/src/EngineEvents.js +2 -0
- package/src/EngineIssues.js +2 -0
- package/src/Headless.js +6 -5
- package/src/Passes/AIUpscaler.js +58 -141
- package/src/Passes/OIDNDenoiser.js +92 -65
- package/src/Passes/OIDNTemporalHistory.js +1 -1
- package/src/PathTracerApp.js +620 -4026
- package/src/Pipeline/PipelineContext.js +0 -4
- package/src/Platform.js +36 -0
- package/src/Processor/ArchiveCache.js +20 -7
- package/src/Processor/ArchiveImporter.js +1265 -0
- package/src/Processor/ArchiveReader.js +22 -4
- package/src/Processor/AssetLoader.js +327 -1763
- package/src/Processor/AtmosphereModel.js +1 -2
- package/src/Processor/BVHBuilder.js +14 -126
- package/src/Processor/BVHLeafFold.js +4 -6
- package/src/Processor/BVHRefitter.js +67 -46
- package/src/Processor/BufferLayout.js +291 -0
- package/src/Processor/EmissiveTriangleBuilder.js +1 -1
- package/src/Processor/EnvironmentExactTable.js +187 -0
- package/src/Processor/EquirectHDRInfo.js +30 -223
- package/src/Processor/FileFormats.js +116 -0
- package/src/Processor/GLTFDecoders.js +85 -0
- package/src/Processor/GLTFDiffuseTransmission.js +63 -0
- package/src/Processor/GeometryExtractor.js +15 -5
- package/src/Processor/HostMemory.js +7 -1
- package/src/Processor/IESParser.js +47 -35
- package/src/Processor/InstanceTable.js +1 -1
- package/src/Processor/KernelManager.js +35 -2
- package/src/Processor/MaterialPacking.js +10 -8
- package/src/Processor/PBRT/PBRTMaterials.js +254 -42
- package/src/Processor/PBRT/PBRTParser.js +49 -19
- package/src/Processor/PBRT/PBRTScattering.js +93 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +1050 -75
- package/src/Processor/PBRT/PBRTTextureBake.js +179 -0
- package/src/Processor/PBRT/PFM.js +59 -0
- package/src/Processor/PackedRayBuffer.js +116 -9
- package/src/Processor/ParallelBVHBuilder.js +3 -6
- package/src/Processor/PhysicalSky.js +5 -4
- package/src/Processor/PlatformImageLoader.js +50 -6
- package/src/Processor/QueueManager.js +39 -5
- package/src/Processor/ReinsertionOptimizer.js +2 -2
- package/src/Processor/SceneMetadata.js +30 -5
- package/src/Processor/SceneProcessor.js +53 -62
- package/src/Processor/ShaderBuilder.js +6 -16
- package/src/Processor/SimpleSky.js +1 -62
- package/src/Processor/SolarLimb.js +3 -0
- package/src/Processor/SplitBLAS.js +1 -1
- package/src/Processor/StorageTexturePool.js +22 -1
- package/src/Processor/TLASBuilder.js +1 -3
- package/src/Processor/TextureBuckets.js +237 -0
- package/src/Processor/TextureCreator.js +181 -8
- package/src/Processor/ToneMapGPU.js +64 -15
- package/src/Processor/Workers/BVHRefitWorker.js +5 -2
- package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
- package/src/Processor/Workers/BVHWorker.js +5 -18
- package/src/Processor/Workers/CDFWorker.js +6 -183
- package/src/Processor/Workers/TexturesWorker.js +103 -18
- package/src/Processor/archiveFormats.js +7 -0
- package/src/Processor/utils.js +2 -1
- package/src/RayzeeRenderer.js +3900 -0
- package/src/RenderSettings.js +68 -62
- package/src/Stages/ASVGF.js +13 -14
- package/src/Stages/AutoExposure.js +17 -17
- package/src/Stages/BilateralFilter.js +2 -1
- package/src/Stages/Compositor.js +8 -23
- package/src/Stages/DenoiserSettings.js +182 -0
- package/src/Stages/EdgeFilter.js +2 -1
- package/src/Stages/MotionVector.js +1 -1
- package/src/Stages/NRD.js +11 -5
- package/src/Stages/NormalDepth.js +12 -8
- package/src/Stages/PathTracer.js +336 -43
- package/src/Stages/PathTracerStage.js +69 -66
- package/src/Stages/Variance.js +1 -1
- package/src/Storage/BLASCache.js +1 -1
- package/src/Storage/CDFCache.js +18 -17
- package/src/Storage/DownloadCache.js +1 -1
- package/src/Storage/SceneGraphCodec.js +46 -3
- package/src/Storage/SpillStore.js +1 -1
- package/src/Storage/StorageManager.js +45 -9
- package/src/Storage/areas.js +4 -0
- package/src/TSL/BVHTraversal.js +50 -25
- package/src/TSL/Bidirectional.js +208 -0
- package/src/TSL/BidirectionalLamps.js +132 -0
- package/src/TSL/Clearcoat.js +15 -6
- package/src/TSL/Common.js +20 -15
- package/src/TSL/ConnectKernel.js +167 -0
- package/src/TSL/Debugger.js +1 -1
- package/src/TSL/Displacement.js +4 -3
- package/src/TSL/EmissiveSampling.js +65 -18
- package/src/TSL/Environment.js +77 -106
- package/src/TSL/EnvironmentCDF.js +85 -39
- package/src/TSL/ExtendKernel.js +8 -3
- package/src/TSL/FinalWriteKernel.js +4 -4
- package/src/TSL/GenerateKernel.js +18 -4
- package/src/TSL/HitFacet.js +35 -5
- package/src/TSL/LightBVHSampling.js +40 -9
- package/src/TSL/LightGenerateKernel.js +293 -0
- package/src/TSL/LightGuide.js +198 -0
- package/src/TSL/LightSplatKernel.js +180 -0
- package/src/TSL/LightVisibility.js +111 -0
- package/src/TSL/LightsCore.js +13 -38
- package/src/TSL/LightsDirect.js +28 -29
- package/src/TSL/LightsIndirect.js +3 -1
- package/src/TSL/LightsSampling.js +239 -51
- package/src/TSL/MaterialEvaluation.js +113 -31
- package/src/TSL/MaterialProperties.js +44 -17
- package/src/TSL/MaterialTransmission.js +52 -47
- package/src/TSL/MergeKernel.js +224 -0
- package/src/TSL/PathTracerCore.js +65 -29
- package/src/TSL/Random.js +5 -1
- package/src/TSL/SceneResources.js +51 -0
- package/src/TSL/ShadeKernel.js +985 -101
- package/src/TSL/Struct.js +10 -0
- package/src/TSL/Sun.js +1 -1
- package/src/TSL/TextureSampling.js +66 -61
- package/src/addons/archives.js +14 -0
- package/src/addons/bidirectional.js +13 -0
- package/src/addons/color.js +32 -0
- package/src/addons/formats.js +16 -0
- package/src/addons/physicalSky.js +14 -0
- package/src/addons/storage.js +19 -0
- package/src/core.js +31 -0
- package/src/index.js +13 -23
- package/src/integrators/BidirectionalIntegrator.js +838 -0
- package/src/managers/AnimationManager.js +88 -6
- package/src/managers/CameraManager.js +103 -46
- package/src/managers/DenoisingManager.js +14 -10
- package/src/managers/EnvironmentManager.js +125 -103
- package/src/managers/GoboManager.js +9 -0
- package/src/managers/MaterialDataManager.js +48 -37
- package/src/managers/UniformManager.js +32 -10
- package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
- package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
- package/src/Processor/TreeletOptimizer.js +0 -574
|
@@ -17,17 +17,33 @@
|
|
|
17
17
|
import { freeNow, resized } from './buffers.js';
|
|
18
18
|
import {
|
|
19
19
|
Group, Mesh, InstancedMesh, PerspectiveCamera, OrthographicCamera, Matrix4, Vector3, Quaternion,
|
|
20
|
-
BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute,
|
|
20
|
+
BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute,
|
|
21
21
|
DataTexture, FloatType, RGBAFormat, LinearFilter, EquirectangularReflectionMapping,
|
|
22
|
-
SRGBColorSpace, AnimationClip, VectorKeyframeTrack, QuaternionKeyframeTrack,
|
|
23
|
-
NumberKeyframeTrack, BooleanKeyframeTrack
|
|
22
|
+
SRGBColorSpace, NoColorSpace, AnimationClip, VectorKeyframeTrack, QuaternionKeyframeTrack,
|
|
23
|
+
NumberKeyframeTrack, BooleanKeyframeTrack, DirectionalLight, PointLight, SpotLight, FrontSide, DoubleSide,
|
|
24
|
+
RepeatWrapping
|
|
24
25
|
} from 'three';
|
|
25
|
-
import { buildMaterial, pFloat, pString, resolveSpectrum } from './PBRTMaterials.js';
|
|
26
|
+
import { buildMaterial, pBool, pFloat, pString, resolveSpectrum } from './PBRTMaterials.js';
|
|
27
|
+
import { makeLayer, layerMean, bake, mixOf, srgbToLinear, linearToSRGB, hasAlpha } from './PBRTTextureBake.js';
|
|
26
28
|
import { loopSubdivide } from './LoopSubdivision.js';
|
|
27
29
|
import { octahedralToEquirect } from './EqualAreaOctahedral.js';
|
|
28
30
|
import { tessellateCurve } from './PBRTCurves.js';
|
|
29
31
|
import * as M from './PBRTMath.js';
|
|
30
32
|
|
|
33
|
+
const LAMP_TYPES = new Set( [ 'distant', 'point', 'spot' ] );
|
|
34
|
+
|
|
35
|
+
// pbrt scales a light's spectrum to luminance 1 unless it is RGB, so a blackbody of any temperature is equally bright.
|
|
36
|
+
async function lightRGB( params, name, ctx ) {
|
|
37
|
+
|
|
38
|
+
const p = params[ name ];
|
|
39
|
+
if ( p?.type === 'spectrum' && typeof p.value[ 0 ] === 'string' && p.value[ 0 ].startsWith( 'stdillum' ) ) return [ 1, 1, 1 ];
|
|
40
|
+
const rgb = ( await resolveSpectrum( params, name, ctx, [ 1, 1, 1 ] ) ).rgb || [ 1, 1, 1 ];
|
|
41
|
+
if ( ! p || p.type === 'rgb' || p.type === 'color' || p.type === 'float' ) return rgb;
|
|
42
|
+
const y = 0.2126 * rgb[ 0 ] + 0.7152 * rgb[ 1 ] + 0.0722 * rgb[ 2 ];
|
|
43
|
+
return y > 0 ? rgb.map( v => v / y ) : rgb;
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
const FLIP_Z = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, 1 ];
|
|
32
48
|
const MAX_SKY_READBACK = 4096;
|
|
33
49
|
|
|
@@ -75,11 +91,7 @@ const DEFAULT_TRIANGLE_BUDGET = 45_000_000;
|
|
|
75
91
|
// Placements are their own budget: geometry is shared, but each costs a TLAS leaf and an
|
|
76
92
|
// instance record. isCoastline's 5.09M loaded and rendered at 23 fps.
|
|
77
93
|
const DEFAULT_PLACEMENT_BUDGET = 6_000_000;
|
|
78
|
-
|
|
79
|
-
// passes these instead. The whole 15-part Moana subset (55.7M / 7.0M) loads cold under them, at
|
|
80
|
-
// an 8.1 GB preflight estimate; the preflight, not the budget, refuses anything larger.
|
|
81
|
-
export const SPILL_TRIANGLE_BUDGET = 60_000_000;
|
|
82
|
-
export const SPILL_PLACEMENT_BUDGET = 8_000_000;
|
|
94
|
+
export { SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from '../HostMemory.js';
|
|
83
95
|
// Non-instanced shapes merge into a few large meshes: isPalmRig declares 173,338 of them,
|
|
84
96
|
// and one BufferGeometry + Mesh each measured 3.3 GB — the loader OOM'd before tracing.
|
|
85
97
|
const DEFAULT_MERGE_MIN_SHAPES = 256;
|
|
@@ -95,7 +107,7 @@ const DEFAULT_CURVE_SIDES = { flat: 1, ribbon: 1, cylinder: 2 };
|
|
|
95
107
|
const DEFAULT_CURVE_TOLERANCE = 0.05;
|
|
96
108
|
|
|
97
109
|
/** Bumped whenever the same scene files build a different graph, so a stored graph is not reused. */
|
|
98
|
-
export const PBRT_BUILD_REVISION =
|
|
110
|
+
export const PBRT_BUILD_REVISION = 9;
|
|
99
111
|
|
|
100
112
|
function samePlacements( a, b ) {
|
|
101
113
|
|
|
@@ -135,6 +147,357 @@ function pixelsFromDrawable( image, srgb ) {
|
|
|
135
147
|
|
|
136
148
|
}
|
|
137
149
|
|
|
150
|
+
/**
|
|
151
|
+
* A texture's pixels, for baking: `{ data, width, height, channels: 4, topDown, float }`, or null where this runtime
|
|
152
|
+
* cannot read them. A DataTexture's rows run bottom up unless it says flipY; a drawable's top down.
|
|
153
|
+
*/
|
|
154
|
+
function texturePixels( texture ) {
|
|
155
|
+
|
|
156
|
+
const image = texture?.image;
|
|
157
|
+
if ( ! image ) return null;
|
|
158
|
+
|
|
159
|
+
if ( ArrayBuffer.isView( image.data ) ) {
|
|
160
|
+
|
|
161
|
+
return { data: image.data, width: image.width, height: image.height, channels: 4, topDown: texture.flipY === true, float: ! ( image.data instanceof Uint8Array || image.data instanceof Uint8ClampedArray ) };
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { width, height } = image;
|
|
166
|
+
if ( ! width || ! height ) return null;
|
|
167
|
+
let canvas = null;
|
|
168
|
+
if ( typeof OffscreenCanvas !== 'undefined' ) canvas = new OffscreenCanvas( width, height );
|
|
169
|
+
else if ( typeof document !== 'undefined' ) {
|
|
170
|
+
|
|
171
|
+
canvas = document.createElement( 'canvas' );
|
|
172
|
+
canvas.width = width;
|
|
173
|
+
canvas.height = height;
|
|
174
|
+
|
|
175
|
+
} else return null;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
|
|
179
|
+
const ctx = canvas.getContext( '2d', { willReadFrequently: true } );
|
|
180
|
+
if ( ! ctx ) return null;
|
|
181
|
+
ctx.drawImage( image, 0, 0 );
|
|
182
|
+
return { data: ctx.getImageData( 0, 0, width, height ).data, width, height, channels: 4, topDown: true, float: false };
|
|
183
|
+
|
|
184
|
+
} catch {
|
|
185
|
+
|
|
186
|
+
return null;
|
|
187
|
+
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// pbrt's `encoding`: an 8-bit image is sRGB unless it says "linear" (or "gamma g").
|
|
193
|
+
function decoderFor( params ) {
|
|
194
|
+
|
|
195
|
+
const encoding = pString( params, 'encoding', 'sRGB' );
|
|
196
|
+
if ( encoding === 'linear' ) return ( v ) => v;
|
|
197
|
+
const gamma = /^gamma\s+([\d.]+)$/.exec( encoding );
|
|
198
|
+
return gamma ? ( v ) => v ** Number( gamma[ 1 ] ) : srgbToLinear;
|
|
199
|
+
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function uvMapping( params ) {
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
su: pFloat( params, 'uscale', 1 ), sv: pFloat( params, 'vscale', 1 ),
|
|
206
|
+
du: pFloat( params, 'udelta', 0 ), dv: pFloat( params, 'vdelta', 0 )
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function setMapping( texture, { su, sv, du, dv } ) {
|
|
212
|
+
|
|
213
|
+
texture.repeat.set( su, sv );
|
|
214
|
+
texture.offset.set( du, dv );
|
|
215
|
+
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A float image (EXR, HDR, PFM) as 8-bit, the only texel a material map takes here: sRGB-encoded for a colour,
|
|
220
|
+
* linear for data. Any other texture comes back as it is.
|
|
221
|
+
*/
|
|
222
|
+
function eightBit( texture, { srgb } ) {
|
|
223
|
+
|
|
224
|
+
const data = texture?.image?.data;
|
|
225
|
+
if ( ! ArrayBuffer.isView( data ) || data instanceof Uint8Array || data instanceof Uint8ClampedArray ) return texture;
|
|
226
|
+
|
|
227
|
+
const { width, height } = texture.image;
|
|
228
|
+
const bytes = new Uint8Array( width * height * 4 );
|
|
229
|
+
const channels = data.length / ( width * height );
|
|
230
|
+
for ( let i = 0; i < width * height; i ++ ) for ( let c = 0; c < 4; c ++ ) {
|
|
231
|
+
|
|
232
|
+
const v = c < channels ? Math.min( 1, Math.max( 0, data[ i * channels + c ] ) ) : 1;
|
|
233
|
+
bytes[ i * 4 + c ] = Math.round( 255 * ( srgb && c < 3 ? linearToSRGB( v ) : v ) );
|
|
234
|
+
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const out = new DataTexture( bytes, width, height, RGBAFormat );
|
|
238
|
+
out.flipY = texture.flipY;
|
|
239
|
+
out.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;
|
|
240
|
+
out.wrapS = out.wrapT = RepeatWrapping;
|
|
241
|
+
// No archive path: a stored scene keeps these bytes rather than decoding the float file again.
|
|
242
|
+
out.needsUpdate = true;
|
|
243
|
+
return out;
|
|
244
|
+
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// A baked image as a texture: bytes bottom row first, sRGB unless a data map, in the uv mapping it was baked in.
|
|
248
|
+
function bakedTexture( { data, width, height, mapping }, { srgb = true } = {} ) {
|
|
249
|
+
|
|
250
|
+
const texture = new DataTexture( data, width, height, RGBAFormat );
|
|
251
|
+
texture.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;
|
|
252
|
+
texture.wrapS = texture.wrapT = RepeatWrapping;
|
|
253
|
+
setMapping( texture, mapping );
|
|
254
|
+
texture.needsUpdate = true;
|
|
255
|
+
return texture;
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* pbrt's thin lens as the camera's own effects (CameraManager applies them when the camera is chosen): the same
|
|
261
|
+
* aperture radius in either DOF mode — `dofBlur` for 'look', a full-frame lens and its f-number for 'physical' at one
|
|
262
|
+
* scene unit a metre — focused by hand at pbrt's distance.
|
|
263
|
+
*/
|
|
264
|
+
function lensEffects( camera, radius, focusDistance ) {
|
|
265
|
+
|
|
266
|
+
const ortho = camera.isOrthographicCamera === true;
|
|
267
|
+
const tanHalf = ortho ? 1 : Math.tan( camera.fov * Math.PI / 360 );
|
|
268
|
+
const focalLength = ortho ? 50 : 12 / tanHalf;
|
|
269
|
+
return {
|
|
270
|
+
enableDOF: true,
|
|
271
|
+
focusDistance,
|
|
272
|
+
aperture: focalLength * 0.001 / ( 2 * radius ),
|
|
273
|
+
focalLength,
|
|
274
|
+
apertureScale: 1,
|
|
275
|
+
anamorphicRatio: 1,
|
|
276
|
+
dofBlur: radius / ( focusDistance * tanHalf ),
|
|
277
|
+
autoFocusMode: 'manual',
|
|
278
|
+
afScreenPoint: { x: 0.5, y: 0.5 },
|
|
279
|
+
orthoHeight: ortho ? ( camera.top - camera.bottom ) / camera.zoom : null,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// What the scene asks the renderer for, where it says: the Integrator's maxdepth, the Sampler's pixelsamples, the Film's
|
|
285
|
+
// resolution — scene metadata's `render`.
|
|
286
|
+
function renderRequest( ir ) {
|
|
287
|
+
|
|
288
|
+
const render = {};
|
|
289
|
+
if ( ir.integrator?.maxdepth > 0 ) render.maxBounces = Math.round( ir.integrator.maxdepth );
|
|
290
|
+
if ( ir.sampler?.pixelsamples > 0 ) render.samples = Math.round( ir.sampler.pixelsamples );
|
|
291
|
+
if ( ir.film?.resolutionGiven ) {
|
|
292
|
+
|
|
293
|
+
render.width = ir.film.xresolution;
|
|
294
|
+
render.height = ir.film.yresolution;
|
|
295
|
+
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return Object.keys( render ).length ? render : null;
|
|
299
|
+
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const QUADRIC_SEGMENTS = 48; // around a whole turn
|
|
303
|
+
|
|
304
|
+
// pbrt's quadric parameters, clamped as pbrt clamps them (shapes.h).
|
|
305
|
+
function sphereParams( params ) {
|
|
306
|
+
|
|
307
|
+
const radius = pFloat( params, 'radius', 1 );
|
|
308
|
+
const z0 = pFloat( params, 'zmin', - radius ), z1 = pFloat( params, 'zmax', radius );
|
|
309
|
+
const clamp = ( z ) => Math.min( radius, Math.max( - radius, z ) );
|
|
310
|
+
return { radius, zMin: clamp( Math.min( z0, z1 ) ), zMax: clamp( Math.max( z0, z1 ) ), phiMax: phiMaxOf( params ) };
|
|
311
|
+
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function cylinderParams( params ) {
|
|
315
|
+
|
|
316
|
+
const z0 = pFloat( params, 'zmin', - 1 ), z1 = pFloat( params, 'zmax', 1 );
|
|
317
|
+
return { radius: pFloat( params, 'radius', 1 ), zMin: Math.min( z0, z1 ), zMax: Math.max( z0, z1 ), phiMax: phiMaxOf( params ) };
|
|
318
|
+
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function diskParams( params ) {
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
radius: pFloat( params, 'radius', 1 ), inner: pFloat( params, 'innerradius', 0 ),
|
|
325
|
+
height: pFloat( params, 'height', 0 ), phiMax: phiMaxOf( params )
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const phiMaxOf = ( params ) => Math.min( 360, Math.max( 0, pFloat( params, 'phimax', 360 ) ) ) * Math.PI / 180;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* A quadric as pbrt parameterises it: `at( u, v, p, n )` over [0, 1]², u around the z axis to `phiMax`; the uv is
|
|
334
|
+
* pbrt's own. Triangles face ∂p/∂u × ∂p/∂v, pbrt's normal; a cell edge of no length (a pole, a centre) drops its
|
|
335
|
+
* triangle.
|
|
336
|
+
*/
|
|
337
|
+
function quadric( phiMax, vSegments, at ) {
|
|
338
|
+
|
|
339
|
+
const uSegments = Math.max( 3, Math.ceil( QUADRIC_SEGMENTS * phiMax / ( 2 * Math.PI ) ) );
|
|
340
|
+
const row = uSegments + 1;
|
|
341
|
+
const position = new Float32Array( row * ( vSegments + 1 ) * 3 );
|
|
342
|
+
const normal = new Float32Array( position.length );
|
|
343
|
+
const uv = new Float32Array( row * ( vSegments + 1 ) * 2 );
|
|
344
|
+
const p = new Vector3(), n = new Vector3();
|
|
345
|
+
|
|
346
|
+
for ( let j = 0; j <= vSegments; j ++ ) for ( let i = 0; i <= uSegments; i ++ ) {
|
|
347
|
+
|
|
348
|
+
const k = j * row + i;
|
|
349
|
+
at( i / uSegments, j / vSegments, p, n );
|
|
350
|
+
p.toArray( position, k * 3 );
|
|
351
|
+
n.toArray( normal, k * 3 );
|
|
352
|
+
uv[ k * 2 ] = i / uSegments;
|
|
353
|
+
uv[ k * 2 + 1 ] = j / vSegments;
|
|
354
|
+
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let extent = 0;
|
|
358
|
+
for ( let k = 0; k < position.length; k ++ ) extent = Math.max( extent, Math.abs( position[ k ] ) );
|
|
359
|
+
const apart = ( a, b ) => Math.hypot( position[ a * 3 ] - position[ b * 3 ], position[ a * 3 + 1 ] - position[ b * 3 + 1 ], position[ a * 3 + 2 ] - position[ b * 3 + 2 ] ) > extent * 1e-6;
|
|
360
|
+
const index = [];
|
|
361
|
+
for ( let j = 0; j < vSegments; j ++ ) for ( let i = 0; i < uSegments; i ++ ) {
|
|
362
|
+
|
|
363
|
+
const a = j * row + i, b = a + 1, c = a + row, d = c + 1;
|
|
364
|
+
if ( apart( a, b ) ) index.push( a, b, d );
|
|
365
|
+
if ( apart( c, d ) ) index.push( a, d, c );
|
|
366
|
+
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const geometry = new BufferGeometry();
|
|
370
|
+
geometry.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );
|
|
371
|
+
geometry.setAttribute( 'normal', new Float32BufferAttribute( normal, 3 ) );
|
|
372
|
+
geometry.setAttribute( 'uv', new Float32BufferAttribute( uv, 2 ) );
|
|
373
|
+
geometry.setIndex( new Uint32BufferAttribute( new Uint32Array( index ), 1 ) );
|
|
374
|
+
return geometry;
|
|
375
|
+
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// What pbrt calls a shape's area when it scales an area light's `power`: a quadric's in its own space, a mesh's in the
|
|
379
|
+
// scene's (pbrt keeps triangles transformed).
|
|
380
|
+
function shapeArea( shape, geometry ) {
|
|
381
|
+
|
|
382
|
+
const params = shape.params;
|
|
383
|
+
switch ( shape.type ) {
|
|
384
|
+
|
|
385
|
+
case 'sphere': {
|
|
386
|
+
|
|
387
|
+
const { radius, zMin, zMax, phiMax } = sphereParams( params );
|
|
388
|
+
return phiMax * radius * ( zMax - zMin );
|
|
389
|
+
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
case 'cylinder': {
|
|
393
|
+
|
|
394
|
+
const { radius, zMin, zMax, phiMax } = cylinderParams( params );
|
|
395
|
+
return phiMax * radius * ( zMax - zMin );
|
|
396
|
+
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
case 'disk': {
|
|
400
|
+
|
|
401
|
+
const { radius, inner, phiMax } = diskParams( params );
|
|
402
|
+
return phiMax * 0.5 * ( radius * radius - inner * inner );
|
|
403
|
+
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const world = new Matrix4().fromArray( shape.ctm );
|
|
409
|
+
const position = geometry.getAttribute( 'position' );
|
|
410
|
+
const index = geometry.index;
|
|
411
|
+
const count = index ? index.count : position.count;
|
|
412
|
+
const a = new Vector3(), b = new Vector3(), c = new Vector3();
|
|
413
|
+
let area = 0;
|
|
414
|
+
for ( let t = 0; t + 2 < count; t += 3 ) {
|
|
415
|
+
|
|
416
|
+
a.fromBufferAttribute( position, index ? index.getX( t ) : t ).applyMatrix4( world );
|
|
417
|
+
b.fromBufferAttribute( position, index ? index.getX( t + 1 ) : t + 1 ).applyMatrix4( world );
|
|
418
|
+
c.fromBufferAttribute( position, index ? index.getX( t + 2 ) : t + 2 ).applyMatrix4( world );
|
|
419
|
+
area += b.sub( a ).cross( c.sub( a ) ).length() / 2;
|
|
420
|
+
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return area;
|
|
424
|
+
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const CHECK_TEXELS = 64; // a baked checkerboard's texels along one check
|
|
428
|
+
const BILERP_TEXELS = 64;
|
|
429
|
+
|
|
430
|
+
// `colorAt( s, t )` (linear RGB) over a w × h grid as sRGB bytes, bottom row first, clamped as pbrt clamps an albedo.
|
|
431
|
+
function rasterize( width, height, colorAt ) {
|
|
432
|
+
|
|
433
|
+
const data = new Uint8Array( width * height * 4 );
|
|
434
|
+
for ( let j = 0; j < height; j ++ ) for ( let i = 0; i < width; i ++ ) {
|
|
435
|
+
|
|
436
|
+
const rgb = colorAt( ( i + 0.5 ) / width, ( j + 0.5 ) / height );
|
|
437
|
+
const o = ( j * width + i ) * 4;
|
|
438
|
+
for ( let c = 0; c < 3; c ++ ) data[ o + c ] = Math.round( 255 * linearToSRGB( Math.min( 1, Math.max( 0, rgb[ c ] ) ) ) );
|
|
439
|
+
data[ o + 3 ] = 255;
|
|
440
|
+
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return data;
|
|
444
|
+
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// A clone that keeps the engine's own material properties, which three.js's copy() does not know.
|
|
448
|
+
function cloneMaterial( material ) {
|
|
449
|
+
|
|
450
|
+
const out = material.clone();
|
|
451
|
+
for ( const key of Object.keys( material ) ) if ( ! ( key in out ) ) out[ key ] = material[ key ];
|
|
452
|
+
return out;
|
|
453
|
+
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// pbrt's shape alpha as the engine's blend mode: a ray passes with chance 1 − α, shadow rays too.
|
|
457
|
+
function translucent( material, alpha ) {
|
|
458
|
+
|
|
459
|
+
const out = cloneMaterial( material );
|
|
460
|
+
out.transparent = true;
|
|
461
|
+
out.opacity = material.opacity * alpha;
|
|
462
|
+
return out;
|
|
463
|
+
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const sameMapping = ( texture, { su, sv, du, dv } ) =>
|
|
467
|
+
texture.repeat.x === su && texture.repeat.y === sv && texture.offset.x === du && texture.offset.y === dv;
|
|
468
|
+
|
|
469
|
+
// An 8-bit image's colour with its alpha decoded as pbrt reads it; null when its alpha is opaque throughout.
|
|
470
|
+
function withDecodedAlpha( pixels, decode ) {
|
|
471
|
+
|
|
472
|
+
if ( pixels.float || ! hasAlpha( pixels ) ) return null;
|
|
473
|
+
const { data, width, height } = pixels;
|
|
474
|
+
const lut = new Uint8Array( 256 );
|
|
475
|
+
for ( let v = 0; v < 256; v ++ ) lut[ v ] = Math.round( 255 * Math.min( 1, Math.max( 0, decode( v / 255 ) ) ) );
|
|
476
|
+
|
|
477
|
+
const out = new Uint8Array( width * height * 4 );
|
|
478
|
+
const rowBytes = width * 4;
|
|
479
|
+
for ( let y = 0; y < height; y ++ ) {
|
|
480
|
+
|
|
481
|
+
const src = ( pixels.topDown ? height - 1 - y : y ) * rowBytes, dst = y * rowBytes;
|
|
482
|
+
for ( let x = 0; x < rowBytes; x += 4 ) {
|
|
483
|
+
|
|
484
|
+
out[ dst + x ] = data[ src + x ];
|
|
485
|
+
out[ dst + x + 1 ] = data[ src + x + 1 ];
|
|
486
|
+
out[ dst + x + 2 ] = data[ src + x + 2 ];
|
|
487
|
+
out[ dst + x + 3 ] = lut[ data[ src + x + 3 ] ];
|
|
488
|
+
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const texture = new DataTexture( out, width, height, RGBAFormat );
|
|
494
|
+
texture.colorSpace = SRGBColorSpace;
|
|
495
|
+
texture.wrapS = texture.wrapT = RepeatWrapping;
|
|
496
|
+
texture.needsUpdate = true;
|
|
497
|
+
return texture;
|
|
498
|
+
|
|
499
|
+
}
|
|
500
|
+
|
|
138
501
|
/**
|
|
139
502
|
* Names an unreadable texture may have been derived from. Moana binds colour as
|
|
140
503
|
* `"texture reflectance" "el:part_Color"` and leaves `MakeNamedMaterial "el:part"` in the
|
|
@@ -192,6 +555,10 @@ export class PBRTSceneBuilder {
|
|
|
192
555
|
this._materialBySignature = new Map(); // visual signature -> shared MeshPhysicalMaterial
|
|
193
556
|
this._noUVMaterials = new Map(); // textured material -> its map-less clone
|
|
194
557
|
this._plyCache = new Map(); // filename -> Promise<BufferGeometry|null>
|
|
558
|
+
this._imageCache = new Map(); // filename -> Promise<Texture|null>, decoded once for every named texture
|
|
559
|
+
this._termCache = new Map(); // texture name + channels -> Promise<{rgb}|{layer, tint?}|null>
|
|
560
|
+
this._mediumMaterials = new Map(); // material -> Map(medium name -> material with its attenuation)
|
|
561
|
+
this._alphaMaterials = new Map(); // material -> Map(alpha texture name or #value -> Promise<material>)
|
|
195
562
|
|
|
196
563
|
}
|
|
197
564
|
|
|
@@ -249,11 +616,12 @@ export class PBRTSceneBuilder {
|
|
|
249
616
|
}
|
|
250
617
|
|
|
251
618
|
const shape = ir.shapes[ i ];
|
|
252
|
-
const [ geometry,
|
|
619
|
+
const [ geometry, surface ] = await Promise.all( [
|
|
253
620
|
this._batches ? this._createGeometry( shape ) : this._buildGeometry( shape ),
|
|
254
621
|
this._getMaterial( shape )
|
|
255
622
|
] );
|
|
256
623
|
if ( ! geometry ) continue;
|
|
624
|
+
const sharedMaterial = shape.areaLight ? this._poweredMaterial( shape, geometry, surface ) : surface;
|
|
257
625
|
|
|
258
626
|
// A moving shape needs a node of its own to move.
|
|
259
627
|
if ( this._batches && ! shape.motion && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
|
|
@@ -308,6 +676,7 @@ export class PBRTSceneBuilder {
|
|
|
308
676
|
|
|
309
677
|
// Infinite light → environment
|
|
310
678
|
const environment = await this._buildEnvironment( ir.lights );
|
|
679
|
+
await this._buildLamps( ir.lights, group );
|
|
311
680
|
|
|
312
681
|
this._reportUnsupportedLights( ir.lights );
|
|
313
682
|
|
|
@@ -317,6 +686,7 @@ export class PBRTSceneBuilder {
|
|
|
317
686
|
|
|
318
687
|
return {
|
|
319
688
|
group, camera, environment, animations,
|
|
689
|
+
render: renderRequest( ir ),
|
|
320
690
|
report: this.report,
|
|
321
691
|
meshCount: this.reportedMeshes,
|
|
322
692
|
triangleCount: this.triangleCount,
|
|
@@ -740,7 +1110,7 @@ export class PBRTSceneBuilder {
|
|
|
740
1110
|
if ( ! material ) {
|
|
741
1111
|
|
|
742
1112
|
this.warn( `${name} (${shape.type}, "${shape.material?.type || 'diffuse'}") has a texture map but no UVs — dropping map, using base color` );
|
|
743
|
-
material = sharedMaterial
|
|
1113
|
+
material = cloneMaterial( sharedMaterial );
|
|
744
1114
|
material.map = null;
|
|
745
1115
|
this._noUVMaterials.set( sharedMaterial, material );
|
|
746
1116
|
|
|
@@ -933,6 +1303,58 @@ export class PBRTSceneBuilder {
|
|
|
933
1303
|
|
|
934
1304
|
async _createGeometry( shape ) {
|
|
935
1305
|
|
|
1306
|
+
const geometry = await this._shapeGeometry( shape );
|
|
1307
|
+
return geometry && shape.areaLight && ! pBool( shape.areaLight.params, 'twosided', false ) ? this._facingEmission( shape, geometry ) : geometry;
|
|
1308
|
+
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* A one-sided area light emits on the side pbrt's surface normal faces: towards the vertex normals where the shape
|
|
1313
|
+
* has them, else its winding's side, turned over by ReverseOrientation. The engine emits from a triangle's front,
|
|
1314
|
+
* so a triangle facing the other way is rewound, on an index of its own: the geometry may be shared.
|
|
1315
|
+
* @private
|
|
1316
|
+
*/
|
|
1317
|
+
_facingEmission( shape, geometry ) {
|
|
1318
|
+
|
|
1319
|
+
const normal = geometry.userData.pbrtNormals ? geometry.getAttribute( 'normal' ) : null;
|
|
1320
|
+
if ( ! normal && ! shape.reverseOrientation ) return geometry;
|
|
1321
|
+
|
|
1322
|
+
const p = geometry.getAttribute( 'position' ).array, n = normal?.array;
|
|
1323
|
+
const src = geometry.index?.array ?? null;
|
|
1324
|
+
const count = src ? src.length : geometry.getAttribute( 'position' ).count;
|
|
1325
|
+
const index = new Uint32Array( count );
|
|
1326
|
+
let flipped = 0;
|
|
1327
|
+
|
|
1328
|
+
for ( let t = 0; t + 2 < count; t += 3 ) {
|
|
1329
|
+
|
|
1330
|
+
const a = src ? src[ t ] : t, b = src ? src[ t + 1 ] : t + 1, c = src ? src[ t + 2 ] : t + 2;
|
|
1331
|
+
let flip = shape.reverseOrientation;
|
|
1332
|
+
if ( n ) {
|
|
1333
|
+
|
|
1334
|
+
const ux = p[ b * 3 ] - p[ a * 3 ], uy = p[ b * 3 + 1 ] - p[ a * 3 + 1 ], uz = p[ b * 3 + 2 ] - p[ a * 3 + 2 ];
|
|
1335
|
+
const vx = p[ c * 3 ] - p[ a * 3 ], vy = p[ c * 3 + 1 ] - p[ a * 3 + 1 ], vz = p[ c * 3 + 2 ] - p[ a * 3 + 2 ];
|
|
1336
|
+
const sx = n[ a * 3 ] + n[ b * 3 ] + n[ c * 3 ], sy = n[ a * 3 + 1 ] + n[ b * 3 + 1 ] + n[ c * 3 + 1 ], sz = n[ a * 3 + 2 ] + n[ b * 3 + 2 ] + n[ c * 3 + 2 ];
|
|
1337
|
+
flip = ( uy * vz - uz * vy ) * sx + ( uz * vx - ux * vz ) * sy + ( ux * vy - uy * vx ) * sz < 0;
|
|
1338
|
+
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
index[ t ] = a;
|
|
1342
|
+
index[ t + 1 ] = flip ? c : b;
|
|
1343
|
+
index[ t + 2 ] = flip ? b : c;
|
|
1344
|
+
if ( flip ) flipped ++;
|
|
1345
|
+
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if ( flipped === 0 ) return geometry;
|
|
1349
|
+
const oriented = new BufferGeometry();
|
|
1350
|
+
for ( const name in geometry.attributes ) oriented.setAttribute( name, geometry.getAttribute( name ) );
|
|
1351
|
+
oriented.setIndex( new Uint32BufferAttribute( index, 1 ) );
|
|
1352
|
+
return oriented;
|
|
1353
|
+
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
_shapeGeometry( shape ) {
|
|
1357
|
+
|
|
936
1358
|
switch ( shape.type ) {
|
|
937
1359
|
|
|
938
1360
|
case 'trianglemesh': return this._triangleMesh( shape.params );
|
|
@@ -942,6 +1364,7 @@ export class PBRTSceneBuilder {
|
|
|
942
1364
|
case 'plymesh': return this._plyMesh( shape.params );
|
|
943
1365
|
case 'sphere': return this._sphere( shape.params );
|
|
944
1366
|
case 'disk': return this._disk( shape.params );
|
|
1367
|
+
case 'cylinder': return this._cylinder( shape.params );
|
|
945
1368
|
default:
|
|
946
1369
|
this.warn( `shape "${shape.type}" not supported — skipped` );
|
|
947
1370
|
return null;
|
|
@@ -963,7 +1386,12 @@ export class PBRTSceneBuilder {
|
|
|
963
1386
|
geo.setAttribute( 'position', new Float32BufferAttribute( float32( P ), 3 ) );
|
|
964
1387
|
|
|
965
1388
|
const N = params.N?.value;
|
|
966
|
-
if ( N && N.length === P.length )
|
|
1389
|
+
if ( N && N.length === P.length ) {
|
|
1390
|
+
|
|
1391
|
+
geo.setAttribute( 'normal', new Float32BufferAttribute( float32( N ), 3 ) );
|
|
1392
|
+
geo.userData.pbrtNormals = true;
|
|
1393
|
+
|
|
1394
|
+
}
|
|
967
1395
|
|
|
968
1396
|
const uv = ( params.uv || params.st )?.value;
|
|
969
1397
|
if ( uv && uv.length === ( P.length / 3 ) * 2 ) geo.setAttribute( 'uv', new Float32BufferAttribute( float32( uv ), 2 ) );
|
|
@@ -1047,28 +1475,75 @@ export class PBRTSceneBuilder {
|
|
|
1047
1475
|
}
|
|
1048
1476
|
|
|
1049
1477
|
// Bilinear patch mesh → triangulate each quad (P + indices in quads of 4).
|
|
1478
|
+
/**
|
|
1479
|
+
* pbrt's bilinear patches as two triangles each: corners p00, p10, p01, p11 (not a ring), facing ∂p/∂u × ∂p/∂v. One
|
|
1480
|
+
* patch of four points needs no indices. Without uvs a patch takes its own (u, v), so its corners are not shared.
|
|
1481
|
+
*/
|
|
1050
1482
|
_bilinearMesh( params ) {
|
|
1051
1483
|
|
|
1052
1484
|
const P = params.P?.value;
|
|
1053
|
-
const quad = params.indices?.value;
|
|
1485
|
+
const quad = params.indices?.value ?? ( P?.length === 12 ? [ 0, 1, 2, 3 ] : null );
|
|
1054
1486
|
if ( ! P || ! quad ) {
|
|
1055
1487
|
|
|
1056
1488
|
this.warn( 'bilinearmesh missing P/indices' ); return null;
|
|
1057
1489
|
|
|
1058
1490
|
}
|
|
1059
1491
|
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1492
|
+
const count = P.length / 3;
|
|
1493
|
+
const uv = params.uv?.value?.length === count * 2 ? params.uv.value : null;
|
|
1494
|
+
const N = params.N?.value?.length === count * 3 ? params.N.value : null;
|
|
1495
|
+
const geo = new BufferGeometry();
|
|
1496
|
+
|
|
1497
|
+
if ( uv ) {
|
|
1498
|
+
|
|
1499
|
+
const index = [];
|
|
1500
|
+
for ( let i = 0; i + 3 < quad.length; i += 4 ) {
|
|
1501
|
+
|
|
1502
|
+
const [ a, b, c, d ] = [ quad[ i ], quad[ i + 1 ], quad[ i + 2 ], quad[ i + 3 ] ];
|
|
1503
|
+
index.push( a, b, d, a, d, c );
|
|
1504
|
+
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
geo.setAttribute( 'position', new Float32BufferAttribute( Float32Array.from( P ), 3 ) );
|
|
1508
|
+
geo.setAttribute( 'uv', new Float32BufferAttribute( Float32Array.from( uv ), 2 ) );
|
|
1509
|
+
if ( N ) geo.setAttribute( 'normal', new Float32BufferAttribute( Float32Array.from( N ), 3 ) );
|
|
1510
|
+
geo.setIndex( new Uint32BufferAttribute( new Uint32Array( index ), 1 ) );
|
|
1511
|
+
|
|
1512
|
+
} else {
|
|
1513
|
+
|
|
1514
|
+
const patches = Math.floor( quad.length / 4 );
|
|
1515
|
+
const position = new Float32Array( patches * 12 ), normal = N ? new Float32Array( patches * 12 ) : null;
|
|
1516
|
+
const corners = new Float32Array( patches * 8 ), index = new Uint32Array( patches * 6 );
|
|
1517
|
+
for ( let i = 0; i < patches; i ++ ) {
|
|
1518
|
+
|
|
1519
|
+
for ( let k = 0; k < 4; k ++ ) {
|
|
1520
|
+
|
|
1521
|
+
const v = quad[ i * 4 + k ];
|
|
1522
|
+
for ( let c = 0; c < 3; c ++ ) {
|
|
1062
1523
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1524
|
+
position[ i * 12 + k * 3 + c ] = P[ v * 3 + c ];
|
|
1525
|
+
if ( normal ) normal[ i * 12 + k * 3 + c ] = N[ v * 3 + c ];
|
|
1526
|
+
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
corners[ i * 8 + k * 2 ] = k & 1;
|
|
1530
|
+
corners[ i * 8 + k * 2 + 1 ] = k >> 1;
|
|
1531
|
+
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
index.set( [ 0, 1, 3, 0, 3, 2 ].map( ( k ) => i * 4 + k ), i * 6 );
|
|
1535
|
+
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
geo.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );
|
|
1539
|
+
geo.setAttribute( 'uv', new Float32BufferAttribute( corners, 2 ) );
|
|
1540
|
+
if ( normal ) geo.setAttribute( 'normal', new Float32BufferAttribute( normal, 3 ) );
|
|
1541
|
+
geo.setIndex( new Uint32BufferAttribute( index, 1 ) );
|
|
1065
1542
|
|
|
1066
1543
|
}
|
|
1067
1544
|
|
|
1068
|
-
|
|
1069
|
-
geo.
|
|
1070
|
-
geo.setIndex( tris );
|
|
1071
|
-
geo.computeVertexNormals();
|
|
1545
|
+
if ( N ) geo.userData.pbrtNormals = true;
|
|
1546
|
+
else geo.computeVertexNormals();
|
|
1072
1547
|
return geo;
|
|
1073
1548
|
|
|
1074
1549
|
}
|
|
@@ -1100,7 +1575,8 @@ export class PBRTSceneBuilder {
|
|
|
1100
1575
|
|
|
1101
1576
|
}
|
|
1102
1577
|
|
|
1103
|
-
if (
|
|
1578
|
+
if ( geo.getAttribute( 'normal' ) ) geo.userData.pbrtNormals = true;
|
|
1579
|
+
else geo.computeVertexNormals();
|
|
1104
1580
|
return geo;
|
|
1105
1581
|
|
|
1106
1582
|
} catch ( e ) {
|
|
@@ -1114,38 +1590,41 @@ export class PBRTSceneBuilder {
|
|
|
1114
1590
|
|
|
1115
1591
|
_sphere( params ) {
|
|
1116
1592
|
|
|
1117
|
-
const radius =
|
|
1118
|
-
|
|
1593
|
+
const { radius, zMin, zMax, phiMax } = sphereParams( params );
|
|
1594
|
+
const thetaMin = Math.acos( zMin / radius ), thetaMax = Math.acos( zMax / radius );
|
|
1595
|
+
return quadric( phiMax, Math.max( 1, Math.ceil( 32 * ( thetaMin - thetaMax ) / Math.PI ) ), ( u, v, p, n ) => {
|
|
1596
|
+
|
|
1597
|
+
const phi = u * phiMax, theta = thetaMin + v * ( thetaMax - thetaMin );
|
|
1598
|
+
n.set( Math.sin( theta ) * Math.cos( phi ), Math.sin( theta ) * Math.sin( phi ), Math.cos( theta ) );
|
|
1599
|
+
p.copy( n ).multiplyScalar( radius );
|
|
1600
|
+
|
|
1601
|
+
} );
|
|
1602
|
+
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
_cylinder( params ) {
|
|
1606
|
+
|
|
1607
|
+
const { radius, zMin, zMax, phiMax } = cylinderParams( params );
|
|
1608
|
+
return quadric( phiMax, 1, ( u, v, p, n ) => {
|
|
1609
|
+
|
|
1610
|
+
const phi = u * phiMax;
|
|
1611
|
+
n.set( Math.cos( phi ), Math.sin( phi ), 0 );
|
|
1612
|
+
p.set( radius * n.x, radius * n.y, zMin + v * ( zMax - zMin ) );
|
|
1613
|
+
|
|
1614
|
+
} );
|
|
1119
1615
|
|
|
1120
1616
|
}
|
|
1121
1617
|
|
|
1122
1618
|
_disk( params ) {
|
|
1123
1619
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
const inner = pFloat( params, 'innerradius', 0 );
|
|
1127
|
-
const h = pFloat( params, 'height', 0 );
|
|
1128
|
-
const seg = 48;
|
|
1129
|
-
const pos = [];
|
|
1130
|
-
const idx = [];
|
|
1131
|
-
for ( let i = 0; i < seg; i ++ ) {
|
|
1132
|
-
|
|
1133
|
-
const a0 = ( i / seg ) * Math.PI * 2;
|
|
1134
|
-
const a1 = ( ( i + 1 ) / seg ) * Math.PI * 2;
|
|
1135
|
-
const base = pos.length / 3;
|
|
1136
|
-
pos.push( Math.cos( a0 ) * inner, Math.sin( a0 ) * inner, h );
|
|
1137
|
-
pos.push( Math.cos( a0 ) * radius, Math.sin( a0 ) * radius, h );
|
|
1138
|
-
pos.push( Math.cos( a1 ) * radius, Math.sin( a1 ) * radius, h );
|
|
1139
|
-
pos.push( Math.cos( a1 ) * inner, Math.sin( a1 ) * inner, h );
|
|
1140
|
-
idx.push( base, base + 1, base + 2, base, base + 2, base + 3 );
|
|
1620
|
+
const { radius, inner, height, phiMax } = diskParams( params );
|
|
1621
|
+
return quadric( phiMax, 1, ( u, v, p, n ) => {
|
|
1141
1622
|
|
|
1142
|
-
|
|
1623
|
+
const phi = u * phiMax, r = radius - v * ( radius - inner );
|
|
1624
|
+
p.set( r * Math.cos( phi ), r * Math.sin( phi ), height );
|
|
1625
|
+
n.set( 0, 0, 1 );
|
|
1143
1626
|
|
|
1144
|
-
|
|
1145
|
-
geo.setAttribute( 'position', new Float32BufferAttribute( Float32Array.from( pos ), 3 ) );
|
|
1146
|
-
geo.setIndex( idx );
|
|
1147
|
-
geo.computeVertexNormals();
|
|
1148
|
-
return geo;
|
|
1627
|
+
} );
|
|
1149
1628
|
|
|
1150
1629
|
}
|
|
1151
1630
|
|
|
@@ -1153,6 +1632,80 @@ export class PBRTSceneBuilder {
|
|
|
1153
1632
|
|
|
1154
1633
|
async _getMaterial( shape ) {
|
|
1155
1634
|
|
|
1635
|
+
let material = await this._surfaceMaterial( shape );
|
|
1636
|
+
if ( shape.interior ) material = await this._inMedium( material, shape.interior );
|
|
1637
|
+
return shape.params.alpha ? this._withAlpha( material, shape.params.alpha ) : material;
|
|
1638
|
+
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/** The material under a shape's `alpha`, one per material and alpha. @private */
|
|
1642
|
+
_withAlpha( material, alpha ) {
|
|
1643
|
+
|
|
1644
|
+
const key = alpha.type === 'texture' ? alpha.value[ 0 ] : `#${alpha.value[ 0 ]}`;
|
|
1645
|
+
let byAlpha = this._alphaMaterials.get( material );
|
|
1646
|
+
if ( ! byAlpha ) this._alphaMaterials.set( material, byAlpha = new Map() );
|
|
1647
|
+
if ( ! byAlpha.has( key ) ) byAlpha.set( key, this._createAlphaMaterial( material, alpha ) );
|
|
1648
|
+
return byAlpha.get( key );
|
|
1649
|
+
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
async _createAlphaMaterial( material, alpha ) {
|
|
1653
|
+
|
|
1654
|
+
if ( alpha.type !== 'texture' ) return alpha.value[ 0 ] < 1 ? translucent( material, alpha.value[ 0 ] ) : material;
|
|
1655
|
+
|
|
1656
|
+
const name = alpha.value[ 0 ];
|
|
1657
|
+
const map = await this._alphaMap( material.map, name );
|
|
1658
|
+
if ( ! map ) {
|
|
1659
|
+
|
|
1660
|
+
this.warn( `shape alpha "${name}" could not be read here — drawn opaque` );
|
|
1661
|
+
return material;
|
|
1662
|
+
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
if ( map.constant !== undefined ) return map.constant < 1 ? translucent( material, map.constant ) : material;
|
|
1666
|
+
const out = cloneMaterial( material );
|
|
1667
|
+
out.map = map;
|
|
1668
|
+
out.transparent = true;
|
|
1669
|
+
return out;
|
|
1670
|
+
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* The colour map with the float texture `name` in its alpha channel, `{ constant }` when that texture is one value,
|
|
1675
|
+
* or null when it cannot be read here.
|
|
1676
|
+
* @private
|
|
1677
|
+
*/
|
|
1678
|
+
async _alphaMap( colorMap, name ) {
|
|
1679
|
+
|
|
1680
|
+
const def = this.ir.namedTextures.get( name );
|
|
1681
|
+
const filename = def?.class === 'imagemap' ? pString( def.params, 'filename', null ) : null;
|
|
1682
|
+
// Most often the alpha is the colour map's own image: its colour stays as it is.
|
|
1683
|
+
if ( filename && colorMap?.userData.pbrtImage === filename && pString( def.params, 'mapping', 'uv' ) === 'uv'
|
|
1684
|
+
&& pFloat( def.params, 'scale', 1 ) === 1 && ! pBool( def.params, 'invert', false ) && sameMapping( colorMap, uvMapping( def.params ) ) ) {
|
|
1685
|
+
|
|
1686
|
+
const pixels = texturePixels( colorMap );
|
|
1687
|
+
const own = pixels && withDecodedAlpha( pixels, decoderFor( def.params ) );
|
|
1688
|
+
if ( own ) {
|
|
1689
|
+
|
|
1690
|
+
setMapping( own, uvMapping( def.params ) );
|
|
1691
|
+
return own;
|
|
1692
|
+
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const alpha = await this._bakeTerm( name, 1 );
|
|
1698
|
+
if ( ! alpha ) return null;
|
|
1699
|
+
if ( ! alpha.layer ) return { constant: alpha.rgb[ 0 ] };
|
|
1700
|
+
const color = colorMap ? this._textureLayer( colorMap, 3 ) : null;
|
|
1701
|
+
if ( colorMap && ! color ) return null;
|
|
1702
|
+
const baked = bake( [ color ? { layer: color } : { rgb: [ 1, 1, 1 ] }, alpha ], ( [ c ] ) => c, { alphaOf: ( [ , a ] ) => a[ 0 ] } );
|
|
1703
|
+
return baked && bakedTexture( baked );
|
|
1704
|
+
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
async _surfaceMaterial( shape ) {
|
|
1708
|
+
|
|
1156
1709
|
// Cache by (material, areaLight) object identity — many shapes share a
|
|
1157
1710
|
// NamedMaterial, so this dedupes the build + texture-decode work. Nested
|
|
1158
1711
|
// Map keys on the object refs directly (null is a valid key).
|
|
@@ -1166,11 +1719,7 @@ export class PBRTSceneBuilder {
|
|
|
1166
1719
|
|
|
1167
1720
|
if ( byLight.has( shape.areaLight ) ) return byLight.get( shape.areaLight );
|
|
1168
1721
|
|
|
1169
|
-
const ctx =
|
|
1170
|
-
resolveNamedTexture: ( n ) => this._resolveNamedTexture( n ),
|
|
1171
|
-
namedMaterials: this.ir.namedMaterials,
|
|
1172
|
-
warn: ( m ) => this.warn( m )
|
|
1173
|
-
};
|
|
1722
|
+
const ctx = this._materialContext();
|
|
1174
1723
|
const material = await buildMaterial( shape.material, ctx );
|
|
1175
1724
|
|
|
1176
1725
|
if ( shape.areaLight ) await this._applyAreaLight( material, shape.areaLight, ctx );
|
|
@@ -1181,6 +1730,86 @@ export class PBRTSceneBuilder {
|
|
|
1181
1730
|
|
|
1182
1731
|
}
|
|
1183
1732
|
|
|
1733
|
+
_materialContext() {
|
|
1734
|
+
|
|
1735
|
+
return this._ctx ??= {
|
|
1736
|
+
resolveNamedTexture: ( n ) => this._resolveNamedTexture( n ),
|
|
1737
|
+
namedMaterials: this.ir.namedMaterials,
|
|
1738
|
+
warn: ( m ) => this.warn( m ),
|
|
1739
|
+
floatTextureMean: async ( n ) => {
|
|
1740
|
+
|
|
1741
|
+
const term = await this._bakeTerm( n, 1 );
|
|
1742
|
+
return term ? ( term.layer ? layerMean( term.layer ) : term.rgb[ 0 ] ) : null;
|
|
1743
|
+
|
|
1744
|
+
},
|
|
1745
|
+
bakeMaterialMix: ( a, b, amountName ) => this._bakeMaterialMix( a, b, amountName ),
|
|
1746
|
+
bakeFloatMap: ( params, keys, combine ) => this._floatMap( params, keys, combine ),
|
|
1747
|
+
textureMeanRGB: async ( n ) => {
|
|
1748
|
+
|
|
1749
|
+
const term = await this._bakeTerm( n, 3 );
|
|
1750
|
+
if ( ! term?.layer ) return term?.rgb ?? null;
|
|
1751
|
+
const { data } = term.layer;
|
|
1752
|
+
const sum = [ 0, 0, 0 ];
|
|
1753
|
+
for ( let i = 0; i < data.length; i ++ ) sum[ i % 3 ] += data[ i ];
|
|
1754
|
+
return sum.map( ( v, c ) => v / ( data.length / 3 ) * ( term.tint?.[ c ] ?? 1 ) );
|
|
1755
|
+
|
|
1756
|
+
},
|
|
1757
|
+
resolveNormalMap: async ( filename ) => {
|
|
1758
|
+
|
|
1759
|
+
const image = await this._image( filename );
|
|
1760
|
+
if ( ! image ) return null;
|
|
1761
|
+
const texture = eightBit( image, { srgb: false } );
|
|
1762
|
+
const normalMap = texture === image ? image.clone() : texture;
|
|
1763
|
+
normalMap.colorSpace = NoColorSpace;
|
|
1764
|
+
return normalMap;
|
|
1765
|
+
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* Glass filled with a homogeneous medium: what passes straight through it (σt = σa + σs) becomes Beer–Lambert
|
|
1773
|
+
* attenuation, since the engine traces no scattering inside a solid. A medium in an opaque shape changes nothing.
|
|
1774
|
+
* @private
|
|
1775
|
+
*/
|
|
1776
|
+
async _inMedium( material, name ) {
|
|
1777
|
+
|
|
1778
|
+
if ( ! ( material.transmission > 0 ) ) return material;
|
|
1779
|
+
let byName = this._mediumMaterials.get( material );
|
|
1780
|
+
if ( ! byName ) this._mediumMaterials.set( material, byName = new Map() );
|
|
1781
|
+
if ( ! byName.has( name ) ) byName.set( name, this._attenuated( material, name ) );
|
|
1782
|
+
return byName.get( name );
|
|
1783
|
+
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
async _attenuated( material, name ) {
|
|
1787
|
+
|
|
1788
|
+
const params = this.ir.media?.get( name );
|
|
1789
|
+
const type = params && pString( params, 'type', '' );
|
|
1790
|
+
if ( type !== 'homogeneous' ) {
|
|
1791
|
+
|
|
1792
|
+
this.warn( params ? `medium "${name}" (${type}) is not supported — "${name}" glass left clear` : `medium "${name}" not defined` );
|
|
1793
|
+
return material;
|
|
1794
|
+
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
if ( params.preset ) this.warn( `medium preset "${pString( params, 'preset', '' )}" is not supported — using sigma_a / sigma_s` );
|
|
1798
|
+
const ctx = this._materialContext();
|
|
1799
|
+
const a = ( await resolveSpectrum( params, 'sigma_a', ctx, [ 1, 1, 1 ] ) ).rgb;
|
|
1800
|
+
const s = ( await resolveSpectrum( params, 'sigma_s', ctx, [ 1, 1, 1 ] ) ).rgb;
|
|
1801
|
+
const scale = pFloat( params, 'scale', 1 );
|
|
1802
|
+
const sigma = [ 0, 1, 2 ].map( ( c ) => ( a[ c ] + s[ c ] ) * scale );
|
|
1803
|
+
const max = Math.max( ...sigma );
|
|
1804
|
+
if ( ! ( max > 0 ) ) return material;
|
|
1805
|
+
|
|
1806
|
+
const variant = cloneMaterial( material );
|
|
1807
|
+
variant.attenuationDistance = 1 / max;
|
|
1808
|
+
variant.attenuationColor.setRGB( ...sigma.map( ( v ) => Math.exp( - v / max ) ) );
|
|
1809
|
+
return this._dedupeMaterial( variant );
|
|
1810
|
+
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1184
1813
|
/**
|
|
1185
1814
|
* Collapse materials that render identically onto one instance — an inline `Material` per
|
|
1186
1815
|
* shape is a distinct object every time, so every batch would hold exactly one shape.
|
|
@@ -1190,9 +1819,12 @@ export class PBRTSceneBuilder {
|
|
|
1190
1819
|
|
|
1191
1820
|
const c = material.color, e = material.emissive;
|
|
1192
1821
|
const key = `${c.r},${c.g},${c.b}|${e.r},${e.g},${e.b}|${material.emissiveIntensity}|` +
|
|
1193
|
-
`${material.map ? material.map.uuid : '-'}|${material.roughness}|${material.metalness}|` +
|
|
1822
|
+
`${material.map ? material.map.uuid : '-'}|${material.roughness}|${material.roughnessMap?.uuid ?? '-'}|${material.metalness}|` +
|
|
1194
1823
|
`${material.transmission}|${material.ior}|${material.thickness}|` +
|
|
1195
|
-
`${material.clearcoat}|${material.clearcoatRoughness}|${material.opacity}|${material.side}
|
|
1824
|
+
`${material.clearcoat}|${material.clearcoatRoughness}|${material.clearcoatRoughnessMap?.uuid ?? '-'}|${material.opacity}|${material.side}|` +
|
|
1825
|
+
`${material.normalMap ? material.normalMap.uuid : '-'}|${material.attenuationDistance}|${material.attenuationColor.toArray()}|` +
|
|
1826
|
+
`${material.specularIntensity}|${material.diffuseTransmission ?? 0}|${material.diffuseTransmissionColor?.toArray() ?? '-'}|` +
|
|
1827
|
+
`${material.subsurface ?? 0}|${material.subsurfaceColor?.toArray() ?? '-'}|${material.subsurfaceRadius ?? '-'}|${material.subsurfaceAnisotropy ?? 0}`;
|
|
1196
1828
|
|
|
1197
1829
|
const existing = this._materialBySignature.get( key );
|
|
1198
1830
|
if ( existing ) return existing;
|
|
@@ -1201,13 +1833,31 @@ export class PBRTSceneBuilder {
|
|
|
1201
1833
|
|
|
1202
1834
|
}
|
|
1203
1835
|
|
|
1836
|
+
/**
|
|
1837
|
+
* An area light given as `power`: pbrt scales its radiance to that power over this shape's area (both sides of a
|
|
1838
|
+
* twosided one); an image's mean is not in it, as an emitting image is not supported.
|
|
1839
|
+
* @private
|
|
1840
|
+
*/
|
|
1841
|
+
_poweredMaterial( shape, geometry, material ) {
|
|
1842
|
+
|
|
1843
|
+
const power = pFloat( shape.areaLight.params, 'power', - 1 );
|
|
1844
|
+
if ( ! ( power > 0 ) ) return material;
|
|
1845
|
+
const area = shapeArea( shape, geometry ) * ( pBool( shape.areaLight.params, 'twosided', false ) ? 2 : 1 );
|
|
1846
|
+
if ( ! ( area > 0 ) ) return material;
|
|
1847
|
+
const out = cloneMaterial( material );
|
|
1848
|
+
out.emissiveIntensity = material.emissiveIntensity * power / ( Math.PI * area );
|
|
1849
|
+
return out;
|
|
1850
|
+
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1204
1853
|
async _applyAreaLight( material, areaLight, ctx ) {
|
|
1205
1854
|
|
|
1206
|
-
const
|
|
1855
|
+
const rgb = await lightRGB( areaLight.params, 'L', ctx );
|
|
1207
1856
|
const scale = pFloat( areaLight.params, 'scale', 1 );
|
|
1208
|
-
const rgb = L.rgb || [ 1, 1, 1 ];
|
|
1209
1857
|
material.emissive.setRGB( rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] );
|
|
1210
1858
|
material.emissiveIntensity = scale;
|
|
1859
|
+
// pbrt's diffuse area light is one-sided unless "twosided" (_facingEmission turns the triangles).
|
|
1860
|
+
material.side = pBool( areaLight.params, 'twosided', false ) ? DoubleSide : FrontSide;
|
|
1211
1861
|
|
|
1212
1862
|
}
|
|
1213
1863
|
|
|
@@ -1224,22 +1874,11 @@ export class PBRTSceneBuilder {
|
|
|
1224
1874
|
|
|
1225
1875
|
} else if ( def.class === 'imagemap' ) {
|
|
1226
1876
|
|
|
1227
|
-
|
|
1228
|
-
if ( filename ) {
|
|
1229
|
-
|
|
1230
|
-
try {
|
|
1231
|
-
|
|
1232
|
-
const tex = await this.resolveImage( filename );
|
|
1233
|
-
if ( tex ) result = { texture: tex };
|
|
1234
|
-
else this.warn( `image not found for texture "${name}": ${filename}` );
|
|
1235
|
-
|
|
1236
|
-
} catch ( e ) {
|
|
1877
|
+
result = await this._imageMapTexture( name, def );
|
|
1237
1878
|
|
|
1238
|
-
|
|
1879
|
+
} else if ( def.class === 'mix' ) {
|
|
1239
1880
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
}
|
|
1881
|
+
result = await this._mixTexture( name, def );
|
|
1243
1882
|
|
|
1244
1883
|
} else if ( def.class === 'constant' ) {
|
|
1245
1884
|
|
|
@@ -1247,6 +1886,10 @@ export class PBRTSceneBuilder {
|
|
|
1247
1886
|
if ( v && v.type === 'rgb' ) result = { constant: [ v.value[ 0 ], v.value[ 1 ], v.value[ 2 ] ] };
|
|
1248
1887
|
else if ( v ) result = { constant: [ v.value[ 0 ], v.value[ 0 ], v.value[ 0 ] ] };
|
|
1249
1888
|
|
|
1889
|
+
} else if ( def.class === 'checkerboard' || def.class === 'bilerp' ) {
|
|
1890
|
+
|
|
1891
|
+
result = await this._proceduralTexture( name, def );
|
|
1892
|
+
|
|
1250
1893
|
} else if ( def.class === 'scale' ) {
|
|
1251
1894
|
|
|
1252
1895
|
// Scale = inner_texture * scale_factor. Resolve the inner (recursively if
|
|
@@ -1276,6 +1919,242 @@ export class PBRTSceneBuilder {
|
|
|
1276
1919
|
|
|
1277
1920
|
}
|
|
1278
1921
|
|
|
1922
|
+
/**
|
|
1923
|
+
* pbrt's 2D `checkerboard` and `bilerp`, baked: one period of the checks (two each way), or the bilerp over the unit
|
|
1924
|
+
* square, in the texture's uv mapping. A textured input counts as its mean.
|
|
1925
|
+
* @private
|
|
1926
|
+
*/
|
|
1927
|
+
async _proceduralTexture( name, def ) {
|
|
1928
|
+
|
|
1929
|
+
const params = def.params;
|
|
1930
|
+
const ctx = this._materialContext();
|
|
1931
|
+
const value = async ( key, dflt ) => {
|
|
1932
|
+
|
|
1933
|
+
const p = params[ key ];
|
|
1934
|
+
if ( p?.type === 'texture' ) return ( await ctx.textureMeanRGB( p.value[ 0 ] ) ) ?? dflt;
|
|
1935
|
+
return ( await resolveSpectrum( params, key, ctx, dflt ) ).rgb ?? dflt;
|
|
1936
|
+
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
const mapping = pString( params, 'mapping', 'uv' );
|
|
1940
|
+
if ( mapping !== 'uv' ) this.warn( `texture "${name}": "${mapping}" mapping is not supported — using the mesh's uv` );
|
|
1941
|
+
const { su, sv, du, dv } = uvMapping( params );
|
|
1942
|
+
|
|
1943
|
+
if ( def.class === 'checkerboard' ) {
|
|
1944
|
+
|
|
1945
|
+
if ( pFloat( params, 'dimension', 2 ) !== 2 ) {
|
|
1946
|
+
|
|
1947
|
+
this.warn( `texture "${name}": a 3D checkerboard is not supported` );
|
|
1948
|
+
return null;
|
|
1949
|
+
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
const [ a, b ] = await Promise.all( [ value( 'tex1', [ 1, 1, 1 ] ), value( 'tex2', [ 0, 0, 0 ] ) ] );
|
|
1953
|
+
const n = 2 * CHECK_TEXELS;
|
|
1954
|
+
const data = rasterize( n, n, ( s, t ) => ( ( Math.floor( 2 * s ) + Math.floor( 2 * t ) ) % 2 ? b : a ) );
|
|
1955
|
+
return { texture: bakedTexture( { data, width: n, height: n, mapping: { su: su / 2, sv: sv / 2, du: du / 2, dv: dv / 2 } } ) };
|
|
1956
|
+
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
const [ v00, v01, v10, v11 ] = await Promise.all( [
|
|
1960
|
+
value( 'v00', [ 0, 0, 0 ] ), value( 'v01', [ 1, 1, 1 ] ), value( 'v10', [ 0, 0, 0 ] ), value( 'v11', [ 1, 1, 1 ] )
|
|
1961
|
+
] );
|
|
1962
|
+
const data = rasterize( BILERP_TEXELS, BILERP_TEXELS, ( s, t ) => [ 0, 1, 2 ].map( ( c ) =>
|
|
1963
|
+
( 1 - s ) * ( 1 - t ) * v00[ c ] + s * ( 1 - t ) * v10[ c ] + ( 1 - s ) * t * v01[ c ] + s * t * v11[ c ] ) );
|
|
1964
|
+
return { texture: bakedTexture( { data, width: BILERP_TEXELS, height: BILERP_TEXELS, mapping: { su, sv, du, dv } } ) };
|
|
1965
|
+
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/** A file's image, decoded once however many named textures read it. @private */
|
|
1969
|
+
_image( filename ) {
|
|
1970
|
+
|
|
1971
|
+
let pending = this._imageCache.get( filename );
|
|
1972
|
+
if ( ! pending ) {
|
|
1973
|
+
|
|
1974
|
+
pending = Promise.resolve().then( () => this.resolveImage( filename ) ).catch( ( e ) => {
|
|
1975
|
+
|
|
1976
|
+
this.warn( `failed to load image ${filename}: ${e.message}` );
|
|
1977
|
+
return null;
|
|
1978
|
+
|
|
1979
|
+
} );
|
|
1980
|
+
this._imageCache.set( filename, pending );
|
|
1981
|
+
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
return pending;
|
|
1985
|
+
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* An image texture with pbrt's uv mapping; its `scale` becomes the material's tint, except that a scale above 1 or
|
|
1990
|
+
* `invert` is baked (pbrt clamps an albedo per texel, which a tint cannot).
|
|
1991
|
+
* @private
|
|
1992
|
+
*/
|
|
1993
|
+
async _imageMapTexture( name, def ) {
|
|
1994
|
+
|
|
1995
|
+
const filename = pString( def.params, 'filename', null );
|
|
1996
|
+
if ( ! filename ) return null;
|
|
1997
|
+
const image = await this._image( filename );
|
|
1998
|
+
if ( ! image ) {
|
|
1999
|
+
|
|
2000
|
+
this.warn( `image not found for texture "${name}": ${filename}` );
|
|
2001
|
+
return null;
|
|
2002
|
+
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
const mapping = pString( def.params, 'mapping', 'uv' );
|
|
2006
|
+
if ( mapping !== 'uv' ) this.warn( `texture "${name}": "${mapping}" mapping is not supported — using the mesh's uv` );
|
|
2007
|
+
|
|
2008
|
+
const scale = pFloat( def.params, 'scale', 1 );
|
|
2009
|
+
if ( scale > 1 || pBool( def.params, 'invert', false ) ) {
|
|
2010
|
+
|
|
2011
|
+
const term = await this._bakeTerm( name, 3 );
|
|
2012
|
+
const baked = term?.layer && bake( [ { ...term, clamp: true } ], ( [ v ] ) => v );
|
|
2013
|
+
if ( baked ) return { texture: bakedTexture( baked ) };
|
|
2014
|
+
this.warn( `texture "${name}": its scale or invert could not be baked here — ignored` );
|
|
2015
|
+
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
const color = eightBit( image, { srgb: true } );
|
|
2019
|
+
const texture = color === image ? image.clone() : color;
|
|
2020
|
+
texture.userData.pbrtImage = filename;
|
|
2021
|
+
setMapping( texture, uvMapping( def.params ) );
|
|
2022
|
+
texture.needsUpdate = true;
|
|
2023
|
+
return scale < 1 ? { texture, constant: [ scale, scale, scale ] } : { texture };
|
|
2024
|
+
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
/**
|
|
2028
|
+
* A data map (linear, in every channel) of `combine` over float parameters `keys`, constants or textures; null when
|
|
2029
|
+
* one cannot be read here or all are constants.
|
|
2030
|
+
* @private
|
|
2031
|
+
*/
|
|
2032
|
+
async _floatMap( params, keys, combine ) {
|
|
2033
|
+
|
|
2034
|
+
const terms = await Promise.all( keys.map( ( key ) => this._paramTerm( params, key, 1, [ 0, 0, 0 ] ) ) );
|
|
2035
|
+
if ( terms.some( ( term ) => ! term ) ) return null;
|
|
2036
|
+
const baked = bake( terms, ( values ) => {
|
|
2037
|
+
|
|
2038
|
+
const v = combine( values.map( ( value ) => value[ 0 ] ) );
|
|
2039
|
+
return [ v, v, v ];
|
|
2040
|
+
|
|
2041
|
+
}, { linear: true } );
|
|
2042
|
+
return baked && bakedTexture( baked, { srgb: false } );
|
|
2043
|
+
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
/** pbrt's `mix` texture, ( 1 − amount ) · tex1 + amount · tex2, baked when any of the three is an image. @private */
|
|
2047
|
+
async _mixTexture( name, def ) {
|
|
2048
|
+
|
|
2049
|
+
const [ a, b, amount ] = await Promise.all( [
|
|
2050
|
+
this._paramTerm( def.params, 'tex1', 3, [ 0, 0, 0 ] ),
|
|
2051
|
+
this._paramTerm( def.params, 'tex2', 3, [ 1, 1, 1 ] ),
|
|
2052
|
+
this._paramTerm( def.params, 'amount', 1, [ 0.5, 0.5, 0.5 ] )
|
|
2053
|
+
] );
|
|
2054
|
+
if ( ! a || ! b || ! amount ) {
|
|
2055
|
+
|
|
2056
|
+
this.warn( `texture "${name}": a "mix" input could not be read here` );
|
|
2057
|
+
return null;
|
|
2058
|
+
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
const unit = ( rgb ) => rgb.map( ( v ) => Math.min( 1, Math.max( 0, v ) ) );
|
|
2062
|
+
const baked = bake( [ { ...a, clamp: true }, { ...b, clamp: true }, amount ], mixOf );
|
|
2063
|
+
return baked ? { texture: bakedTexture( baked ) } : { constant: mixOf( [ unit( a.rgb ), unit( b.rgb ), amount.rgb ] ) };
|
|
2064
|
+
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
/** A texture parameter as a bake term: a constant, or the texture it names. @private */
|
|
2068
|
+
async _paramTerm( params, key, channels, dflt ) {
|
|
2069
|
+
|
|
2070
|
+
const p = params[ key ];
|
|
2071
|
+
if ( ! p ) return { rgb: dflt };
|
|
2072
|
+
if ( p.type === 'texture' ) return this._bakeTerm( p.value[ 0 ], channels );
|
|
2073
|
+
const rgb = ( await resolveSpectrum( params, key, this._materialContext(), dflt ) ).rgb;
|
|
2074
|
+
return rgb ? { rgb } : null;
|
|
2075
|
+
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* A named texture as pbrt evaluates it, for baking: `{ rgb }` for a constant, `{ layer, tint? }` for an image;
|
|
2080
|
+
* null when it cannot be read in this runtime.
|
|
2081
|
+
* @param {string} name
|
|
2082
|
+
* @param {number} channels - 1 where it is read as a float texture, 3 as a spectrum
|
|
2083
|
+
* @private
|
|
2084
|
+
*/
|
|
2085
|
+
_bakeTerm( name, channels ) {
|
|
2086
|
+
|
|
2087
|
+
const key = `${name}\0${channels}`;
|
|
2088
|
+
let pending = this._termCache.get( key );
|
|
2089
|
+
if ( ! pending ) this._termCache.set( key, pending = this._createBakeTerm( name, channels ) );
|
|
2090
|
+
return pending;
|
|
2091
|
+
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
async _createBakeTerm( name, channels ) {
|
|
2095
|
+
|
|
2096
|
+
const def = this.ir.namedTextures.get( name );
|
|
2097
|
+
if ( def?.class === 'imagemap' ) {
|
|
2098
|
+
|
|
2099
|
+
if ( pString( def.params, 'mapping', 'uv' ) !== 'uv' ) return null;
|
|
2100
|
+
const filename = pString( def.params, 'filename', null );
|
|
2101
|
+
const pixels = texturePixels( filename && await this._image( filename ) );
|
|
2102
|
+
if ( ! pixels ) return null;
|
|
2103
|
+
return { layer: makeLayer( pixels, {
|
|
2104
|
+
channels, decode: pixels.float ? null : decoderFor( def.params ),
|
|
2105
|
+
scale: pFloat( def.params, 'scale', 1 ), invert: pBool( def.params, 'invert', false ), mapping: uvMapping( def.params )
|
|
2106
|
+
} ) };
|
|
2107
|
+
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
const resolved = await this._resolveNamedTexture( name );
|
|
2111
|
+
if ( ! resolved ) return null;
|
|
2112
|
+
if ( ! resolved.texture ) return resolved.constant ? { rgb: resolved.constant } : null;
|
|
2113
|
+
const layer = this._textureLayer( resolved.texture, channels );
|
|
2114
|
+
return layer ? { layer, tint: resolved.constant ?? null } : null;
|
|
2115
|
+
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// A texture already built for a material (an image or a bake) as a layer, in the mapping it carries.
|
|
2119
|
+
_textureLayer( texture, channels ) {
|
|
2120
|
+
|
|
2121
|
+
const pixels = texturePixels( texture );
|
|
2122
|
+
if ( ! pixels ) return null;
|
|
2123
|
+
const srgb = ! pixels.float && texture.colorSpace === SRGBColorSpace;
|
|
2124
|
+
return makeLayer( pixels, {
|
|
2125
|
+
channels, decode: pixels.float ? null : ( srgb ? srgbToLinear : ( v ) => v ),
|
|
2126
|
+
mapping: { su: texture.repeat.x, sv: texture.repeat.y, du: texture.offset.x, dv: texture.offset.y }
|
|
2127
|
+
} );
|
|
2128
|
+
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/**
|
|
2132
|
+
* A mix material whose amount is a texture: its two materials' colours (map × colour) baked by that texture, and
|
|
2133
|
+
* the texture's mean to weigh everything else. Null when nothing here can read it.
|
|
2134
|
+
* @private
|
|
2135
|
+
*/
|
|
2136
|
+
async _bakeMaterialMix( matA, matB, amountName ) {
|
|
2137
|
+
|
|
2138
|
+
const amount = await this._bakeTerm( amountName, 1 );
|
|
2139
|
+
if ( ! amount ) return null;
|
|
2140
|
+
if ( ! amount.layer ) return { mean: amount.rgb[ 0 ], texture: null };
|
|
2141
|
+
|
|
2142
|
+
const mean = layerMean( amount.layer );
|
|
2143
|
+
const term = ( m ) => {
|
|
2144
|
+
|
|
2145
|
+
if ( ! m.map ) return { rgb: m.color.toArray(), clamp: true };
|
|
2146
|
+
const layer = this._textureLayer( m.map, 3 );
|
|
2147
|
+
return layer && { layer, tint: m.color.toArray(), clamp: true };
|
|
2148
|
+
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
const a = term( matA ), b = term( matB );
|
|
2152
|
+
if ( ! a || ! b ) return { mean, texture: null };
|
|
2153
|
+
const baked = bake( [ a, b, amount ], mixOf );
|
|
2154
|
+
return { mean, texture: baked ? bakedTexture( baked ) : null };
|
|
2155
|
+
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1279
2158
|
_colorFromLikeNamedMaterial( texName ) {
|
|
1280
2159
|
|
|
1281
2160
|
for ( const candidate of materialNameCandidates( texName ) ) {
|
|
@@ -1297,6 +2176,25 @@ export class PBRTSceneBuilder {
|
|
|
1297
2176
|
async _resolveScaleTexture( name, def ) {
|
|
1298
2177
|
|
|
1299
2178
|
// pbrt-v4 uses "tex" (the inner texture or constant) and "scale" (the multiplier).
|
|
2179
|
+
if ( def.params.scale?.type === 'texture' ) {
|
|
2180
|
+
|
|
2181
|
+
// A texture multiplier (kroken's bricks: colour × dirt) is baked.
|
|
2182
|
+
const [ inner, factor ] = await Promise.all( [
|
|
2183
|
+
this._paramTerm( def.params, 'tex', 3, [ 1, 1, 1 ] ),
|
|
2184
|
+
this._paramTerm( def.params, 'scale', 1, [ 1, 1, 1 ] )
|
|
2185
|
+
] );
|
|
2186
|
+
const product = ( [ a, f ] ) => [ a[ 0 ] * f[ 0 ], a[ 1 ] * f[ 0 ], a[ 2 ] * f[ 0 ] ];
|
|
2187
|
+
if ( inner && factor ) {
|
|
2188
|
+
|
|
2189
|
+
const baked = bake( [ { ...inner, clamp: true }, factor ], product );
|
|
2190
|
+
return baked ? { texture: bakedTexture( baked ) } : { constant: product( [ inner.rgb, factor.rgb ] ) };
|
|
2191
|
+
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
this.warn( `texture "${name}": its "scale" texture could not be read here — ignored` );
|
|
2195
|
+
|
|
2196
|
+
}
|
|
2197
|
+
|
|
1300
2198
|
const innerP = def.params.tex;
|
|
1301
2199
|
let inner = null;
|
|
1302
2200
|
|
|
@@ -1362,6 +2260,8 @@ export class PBRTSceneBuilder {
|
|
|
1362
2260
|
camera.name = 'PBRT Camera';
|
|
1363
2261
|
this._poseCamera( camera, cam.cameraToWorld );
|
|
1364
2262
|
camera.updateMatrixWorld( true );
|
|
2263
|
+
const lensRadius = pFloat( cam.params, 'lensradius', 0 );
|
|
2264
|
+
if ( lensRadius > 0 ) camera.userData.__rayzeeEffects = lensEffects( camera, lensRadius, pFloat( cam.params, 'focaldistance', 1e6 ) );
|
|
1365
2265
|
return camera;
|
|
1366
2266
|
|
|
1367
2267
|
}
|
|
@@ -1500,8 +2400,7 @@ export class PBRTSceneBuilder {
|
|
|
1500
2400
|
|
|
1501
2401
|
// Constant-radiance infinite light → tiny float texture (CDF-buildable).
|
|
1502
2402
|
const ctx = { resolveNamedTexture: async () => null, warn: ( m ) => this.warn( m ) };
|
|
1503
|
-
const
|
|
1504
|
-
const rgb = ( L.rgb || [ 1, 1, 1 ] ).map( v => v * scale );
|
|
2403
|
+
const rgb = ( await lightRGB( inf.params, 'L', ctx ) ).map( v => v * scale );
|
|
1505
2404
|
|
|
1506
2405
|
const w = 2, h = 1;
|
|
1507
2406
|
const data = new Float32Array( w * h * 4 );
|
|
@@ -1523,13 +2422,89 @@ export class PBRTSceneBuilder {
|
|
|
1523
2422
|
|
|
1524
2423
|
}
|
|
1525
2424
|
|
|
2425
|
+
/**
|
|
2426
|
+
* `distant`, `point` and `spot` lights as three.js lamps, in the engine's units: a directional lamp's
|
|
2427
|
+
* intensity is the irradiance it gives (pbrt's L), a point or spot lamp's is 4π × its radiant intensity.
|
|
2428
|
+
*/
|
|
2429
|
+
async _buildLamps( lights, group ) {
|
|
2430
|
+
|
|
2431
|
+
const ctx = { resolveNamedTexture: async () => null, warn: ( m ) => this.warn( m ) };
|
|
2432
|
+
for ( const l of lights ) {
|
|
2433
|
+
|
|
2434
|
+
if ( ! LAMP_TYPES.has( l.type ) ) continue;
|
|
2435
|
+
|
|
2436
|
+
const world = new Matrix4().fromArray( this.convertHandedness ? M.multiply( FLIP_Z, l.ctm ) : l.ctm );
|
|
2437
|
+
const point = ( name, dflt ) => {
|
|
2438
|
+
|
|
2439
|
+
const v = l.params[ name ]?.value;
|
|
2440
|
+
return new Vector3().fromArray( v?.length >= 3 ? v : dflt ).applyMatrix4( world );
|
|
2441
|
+
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
const rgb = await lightRGB( l.params, l.type === 'distant' ? 'L' : 'I', ctx );
|
|
2445
|
+
let scale = pFloat( l.params, 'scale', 1 );
|
|
2446
|
+
const from = point( 'from', [ 0, 0, 0 ] );
|
|
2447
|
+
const to = point( 'to', [ 0, 0, 1 ] );
|
|
2448
|
+
let light;
|
|
2449
|
+
if ( l.type === 'distant' ) {
|
|
2450
|
+
|
|
2451
|
+
const illuminance = pFloat( l.params, 'illuminance', - 1 );
|
|
2452
|
+
if ( illuminance > 0 ) scale *= illuminance;
|
|
2453
|
+
light = new DirectionalLight();
|
|
2454
|
+
light.userData.__luxConverted = true;
|
|
2455
|
+
|
|
2456
|
+
} else if ( l.type === 'point' ) {
|
|
2457
|
+
|
|
2458
|
+
// `power` sets the radiant intensity to power / 4π.
|
|
2459
|
+
const power = pFloat( l.params, 'power', - 1 );
|
|
2460
|
+
scale *= power > 0 ? power : 4 * Math.PI;
|
|
2461
|
+
light = new PointLight();
|
|
2462
|
+
light.userData.__candelaConverted = true;
|
|
2463
|
+
|
|
2464
|
+
} else {
|
|
2465
|
+
|
|
2466
|
+
// pbrt's falloff is smoothstep( cos cone, cos( cone − delta ), cos θ ); the engine's blend is that width over 1 − cos cone.
|
|
2467
|
+
const cone = Math.min( Math.max( pFloat( l.params, 'coneangle', 30 ), 0 ), 90 ) * Math.PI / 180;
|
|
2468
|
+
const delta = Math.min( Math.max( pFloat( l.params, 'conedeltaangle', 5 ) * Math.PI / 180, 0 ), cone );
|
|
2469
|
+
const cosEnd = Math.cos( cone ), cosStart = Math.cos( cone - delta );
|
|
2470
|
+
const power = pFloat( l.params, 'power', - 1 );
|
|
2471
|
+
if ( power > 0 ) scale *= power / ( 2 * Math.PI * ( ( 1 - cosStart ) + ( cosStart - cosEnd ) / 2 ) );
|
|
2472
|
+
scale *= 4 * Math.PI;
|
|
2473
|
+
light = new SpotLight();
|
|
2474
|
+
light.angle = cone;
|
|
2475
|
+
light.penumbra = cosEnd < 1 ? Math.min( ( cosStart - cosEnd ) / ( 1 - cosEnd ), 1 ) : 0;
|
|
2476
|
+
light.userData.__candelaConverted = true;
|
|
2477
|
+
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
const peak = Math.max( rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] );
|
|
2481
|
+
if ( ! ( peak > 0 ) || ! ( scale > 0 ) ) continue;
|
|
2482
|
+
light.color.setRGB( rgb[ 0 ] / peak, rgb[ 1 ] / peak, rgb[ 2 ] / peak );
|
|
2483
|
+
light.intensity = peak * scale;
|
|
2484
|
+
light.name = `pbrt ${l.type}`;
|
|
2485
|
+
light.position.copy( from );
|
|
2486
|
+
light.decay = 2;
|
|
2487
|
+
light.distance = 0;
|
|
2488
|
+
if ( light.target ) {
|
|
2489
|
+
|
|
2490
|
+
light.target.position.subVectors( to, from );
|
|
2491
|
+
light.add( light.target );
|
|
2492
|
+
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
group.add( light );
|
|
2496
|
+
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
}
|
|
2500
|
+
|
|
1526
2501
|
_reportUnsupportedLights( lights ) {
|
|
1527
2502
|
|
|
1528
2503
|
for ( const l of lights ) {
|
|
1529
2504
|
|
|
1530
|
-
if ( l.type !== 'infinite' ) {
|
|
2505
|
+
if ( l.type !== 'infinite' && ! LAMP_TYPES.has( l.type ) ) {
|
|
1531
2506
|
|
|
1532
|
-
this.warn( `light "${l.type}" not supported (only infinite lights and emissive area lights are mapped)` );
|
|
2507
|
+
this.warn( `light "${l.type}" not supported (only infinite, distant, point and spot lights and emissive area lights are mapped)` );
|
|
1533
2508
|
|
|
1534
2509
|
}
|
|
1535
2510
|
|