@volter/blender-engine 0.1.129 → 0.1.131
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/browser/blender-emscripten-engine.mts +42 -10
- package/browser/blender-engine.mts +3 -0
- package/browser/index.ts +1 -1
- package/browser/protocol.ts +45 -0
- package/browser/runtime.ts +33 -1
- package/browser/session.py +237 -32
- package/browser/three/blender-depsgraph-placements.ts +27 -0
- package/browser/three/blender-graph-material.ts +9 -6
- package/browser/three/blender-node-graph.ts +13 -1
- package/browser/three/blender-object-info-materials.ts +20 -5
- package/browser/three/blender-physical-material.ts +14 -1
- package/browser/three/blender-render-camera.ts +22 -0
- package/browser/three/blender-runtime-lighting.ts +56 -7
- package/browser/three/blender-runtime-shadows.ts +5 -0
- package/browser/three/blender-runtime-view.ts +64 -9
- package/browser/three/blender-single-scattering-sky.ts +20 -0
- package/browser/three/blender-sky.ts +26 -1
- package/browser/three/blender-world-visibility.ts +264 -0
- package/browser/three/world-field-sampler.ts +58 -1
- package/browser/volter_three.py +5 -1
- package/browser/worker.ts +47 -2
- package/package.json +1 -1
- package/wasm/BUNDLE.json +15 -15
- package/wasm/blender_browser.data.br +0 -0
- package/wasm/blender_browser.js +1 -1
- package/wasm/blender_browser.wasm.br +0 -0
|
@@ -26,7 +26,7 @@ interface Binding {
|
|
|
26
26
|
channels: Record<string, number>;
|
|
27
27
|
borrowedFrom?: Binding;
|
|
28
28
|
borrowedSource?: THREE.MeshPhysicalMaterial;
|
|
29
|
-
objectInfoUniforms?: {color: THREE.IUniform<THREE.Vector4>; indexRandom: THREE.IUniform<THREE.Vector2>};
|
|
29
|
+
objectInfoUniforms?: {color: THREE.IUniform<THREE.Vector4>; indexRandom: THREE.IUniform<THREE.Vector2>; particleRandom: THREE.IUniform<number>};
|
|
30
30
|
palette?: {indices: Int32Array; sources: readonly THREE.MeshPhysicalMaterial[]};
|
|
31
31
|
}
|
|
32
32
|
const bindings = new WeakMap<THREE.MeshPhysicalMaterial, Binding>();
|
|
@@ -46,7 +46,7 @@ function compatiblePaletteGraphs(sources: readonly THREE.MeshPhysicalMaterial[])
|
|
|
46
46
|
if (sources.length !== 2 || sources.some(source => pendings.has(source))) return null;
|
|
47
47
|
const graphs = sources.map(source => bindings.get(source)?.compiled);
|
|
48
48
|
const graph = graphs[0];
|
|
49
|
-
if (!graph || graph.objectInfo || failed.has(graph.key) || graphs.some(other => !other || other.key !== graph.key ||
|
|
49
|
+
if (!graph || graph.objectInfo || graph.particleInfo || failed.has(graph.key) || graphs.some(other => !other || other.key !== graph.key ||
|
|
50
50
|
other.declarations !== graph.declarations || other.library !== graph.library ||
|
|
51
51
|
JSON.stringify([other.uvs, other.attributes, other.outputs]) !== JSON.stringify([graph.uvs, graph.attributes, graph.outputs]))) return null;
|
|
52
52
|
// Leave room for the physical material's environment and lighting samplers.
|
|
@@ -186,17 +186,20 @@ export function borrowGraphDrawBinding(source: THREE.MeshPhysicalMaterial, targe
|
|
|
186
186
|
* belong to ONE object. A distinct material identity makes Three upload them
|
|
187
187
|
* when it switches between objects sharing an authored material. */
|
|
188
188
|
export function bindGraphObjectInfo(material: THREE.MeshPhysicalMaterial,
|
|
189
|
-
info: {color: readonly number[]; index: number; random: number}): void {
|
|
189
|
+
info: {color: readonly number[]; index: number; random: number; particle_random?: number}): void {
|
|
190
190
|
const binding=bindings.get(material);
|
|
191
|
-
if (!binding
|
|
191
|
+
if (!binding || (!binding.compiled.objectInfo && !binding.compiled.particleInfo)) return;
|
|
192
|
+
if (binding.compiled.particleInfo && info.particle_random === undefined)
|
|
193
|
+
throw new Error('Particle Info Random input missing for Blender object');
|
|
192
194
|
const values=binding.objectInfoUniforms ??= {
|
|
193
|
-
color:{value:new THREE.Vector4()}, indexRandom:{value:new THREE.Vector2()},
|
|
195
|
+
color:{value:new THREE.Vector4()}, indexRandom:{value:new THREE.Vector2()}, particleRandom:{value:0},
|
|
194
196
|
};
|
|
195
197
|
if (binding.uniforms['blenderObjectColor']!==values.color) {
|
|
196
|
-
binding.uniforms={...binding.uniforms,blenderObjectColor:values.color,blenderObjectIndexRandom:values.indexRandom};
|
|
198
|
+
binding.uniforms={...binding.uniforms,blenderObjectColor:values.color,blenderObjectIndexRandom:values.indexRandom, blenderParticleRandom:values.particleRandom};
|
|
197
199
|
}
|
|
198
200
|
values.color.value.fromArray(info.color);
|
|
199
201
|
values.indexRandom.value.set(info.index,info.random);
|
|
202
|
+
if (info.particle_random !== undefined) values.particleRandom.value=info.particle_random;
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
/** A binding's values: uniforms, ramp tables and textures. */
|
|
@@ -49,7 +49,7 @@ export const GRAPH_NODE_TYPES = [
|
|
|
49
49
|
'ShaderNodeGamma', 'ShaderNodeRGBToBW', 'ShaderNodeSeparateColor', 'ShaderNodeCombineColor',
|
|
50
50
|
'ShaderNodeVectorRotate', 'ShaderNodeFresnel', 'ShaderNodeLayerWeight', 'ShaderNodeBump',
|
|
51
51
|
'ShaderNodeNormalMap', 'ShaderNodeNewGeometry', 'ShaderNodeVertexColor', 'ShaderNodeAttribute',
|
|
52
|
-
'ShaderNodeRGBCurve', 'ShaderNodeVectorCurve', 'ShaderNodeFloatCurve', 'ShaderNodeObjectInfo',
|
|
52
|
+
'ShaderNodeRGBCurve', 'ShaderNodeVectorCurve', 'ShaderNodeFloatCurve', 'ShaderNodeObjectInfo', 'ShaderNodeParticleInfo',
|
|
53
53
|
] as const;
|
|
54
54
|
|
|
55
55
|
const nodeSchema = z.object({
|
|
@@ -145,6 +145,7 @@ export interface CompiledGraph {
|
|
|
145
145
|
readonly attributes: readonly string[];
|
|
146
146
|
/** Requires a draw identity with this object's native inputs. */
|
|
147
147
|
readonly objectInfo: boolean;
|
|
148
|
+
readonly particleInfo: boolean;
|
|
148
149
|
/** Which surface inputs the graph drives, each a GLSL global of `type`. */
|
|
149
150
|
readonly outputs: Readonly<Record<string, {readonly global: string; readonly type: GpuType}>>;
|
|
150
151
|
readonly surface: MaterialGraph['surface'];
|
|
@@ -433,6 +434,8 @@ class Compiler {
|
|
|
433
434
|
const [key, index] = source.link;
|
|
434
435
|
const node = this.graph.nodes[key];
|
|
435
436
|
if (!node) throw new Error(`The graph links to ${key}, which it does not carry`);
|
|
437
|
+
if (node.type === 'ShaderNodeParticleInfo' && node.outputs[index]?.id !== 'Random')
|
|
438
|
+
throw new Error(`Particle Info output ${node.outputs[index]?.id ?? index} is not compiled by the presenter`);
|
|
436
439
|
const outputs = this.node(key, node);
|
|
437
440
|
const output = outputs[index];
|
|
438
441
|
if (!output) throw new Error(`${key} has no output ${index}`);
|
|
@@ -489,6 +492,12 @@ class Compiler {
|
|
|
489
492
|
body.push(`${outputs[0]!.name} = ${this.uniform(type, prop<number | number[]>(node, 'value'))};`);
|
|
490
493
|
break;
|
|
491
494
|
}
|
|
495
|
+
case 'ShaderNodeParticleInfo': {
|
|
496
|
+
// Cycles uses the originating particle's index hash, not Object Info
|
|
497
|
+
// Random. The exporter refuses other outputs until their data travels.
|
|
498
|
+
body.push(`${outputs[1]!.name} = blenderParticleRandom;`);
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
492
501
|
case 'ShaderNodeObjectInfo': {
|
|
493
502
|
// node_shader_gpu_object_info / node_object_info. Location is the
|
|
494
503
|
// object's origin, never the current fragment's world position.
|
|
@@ -959,6 +968,8 @@ class Compiler {
|
|
|
959
968
|
const program = [
|
|
960
969
|
...(Object.values(this.graph.nodes).some(n=>n.type==='ShaderNodeObjectInfo')
|
|
961
970
|
? ['uniform vec4 blenderObjectColor;', 'uniform vec2 blenderObjectIndexRandom;'] : []),
|
|
971
|
+
...(Object.values(this.graph.nodes).some(n=>n.type==='ShaderNodeParticleInfo')
|
|
972
|
+
? ['uniform float blenderParticleRandom;'] : []),
|
|
962
973
|
...[...this.s.uniformTypes].map(([n, t]) => `uniform ${t} ${n};`),
|
|
963
974
|
...this.images.map(i => i.tiled
|
|
964
975
|
? `uniform highp sampler2DArray ${i.uniform};\nuniform highp sampler2D ${i.uniform}Map;`
|
|
@@ -980,6 +991,7 @@ class Compiler {
|
|
|
980
991
|
uvs: [...this.s.uvs],
|
|
981
992
|
attributes: [...this.s.attributes],
|
|
982
993
|
objectInfo: Object.values(this.graph.nodes).some(n=>n.type==='ShaderNodeObjectInfo'),
|
|
994
|
+
particleInfo: Object.values(this.graph.nodes).some(n=>n.type==='ShaderNodeParticleInfo'),
|
|
983
995
|
outputs,
|
|
984
996
|
surface: this.graph.surface,
|
|
985
997
|
closure: closure !== undefined,
|
|
@@ -6,6 +6,7 @@ export interface BlenderObjectInfo {
|
|
|
6
6
|
readonly color: readonly number[];
|
|
7
7
|
readonly index: number;
|
|
8
8
|
readonly random: number;
|
|
9
|
+
readonly particle_random?: number;
|
|
9
10
|
}
|
|
10
11
|
type Draw = {source: THREE.MeshPhysicalMaterial; mesh: THREE.Mesh; material: THREE.MeshPhysicalMaterial; version:number};
|
|
11
12
|
|
|
@@ -18,17 +19,24 @@ export class BlenderObjectInfoMaterials {
|
|
|
18
19
|
private readonly draws=new Map<string,Draw>();
|
|
19
20
|
private readonly sources=new Map<THREE.MeshPhysicalMaterial,string|null>();
|
|
20
21
|
private used=new Set<string>();
|
|
22
|
+
private readonly particleInputs=new Map<THREE.Mesh,number>();
|
|
21
23
|
|
|
22
|
-
begin(): void {this.used.clear();this.sources.clear();}
|
|
24
|
+
begin(): void {this.used.clear();this.sources.clear();this.particleInputs.clear();}
|
|
23
25
|
|
|
24
26
|
get(source: THREE.Material, mesh: THREE.Mesh): THREE.Material {
|
|
25
27
|
if (!(source instanceof THREE.MeshPhysicalMaterial)) return source;
|
|
26
28
|
const graph=materialGraph(source);
|
|
27
29
|
this.sources.set(source,graph?.key??null);
|
|
28
|
-
if (!graph
|
|
30
|
+
if (!graph || (!graph.objectInfo && !graph.particleInfo)) return source;
|
|
29
31
|
const info=mesh.userData['blenderObjectInfo'] as BlenderObjectInfo|undefined;
|
|
30
32
|
if (!info) throw new Error(`Object Info inputs missing for Blender object ${mesh.name}`);
|
|
31
|
-
|
|
33
|
+
// Particle Random alone is a draw-uniform value, not a position input.
|
|
34
|
+
// Identical geometry/values (especially Cycles' default child record) share one
|
|
35
|
+
// material identity. Object Info still needs each object's model matrix.
|
|
36
|
+
if (graph.particleInfo && info.particle_random === undefined)
|
|
37
|
+
throw new Error(`Particle Info Random input missing for Blender object ${mesh.name}`);
|
|
38
|
+
if (!graph.objectInfo) this.particleInputs.set(mesh,info.particle_random!);
|
|
39
|
+
const key=`${graph.objectInfo ? mesh.uuid : `particle:${mesh.geometry.uuid}:${info.particle_random}`}:${source.uuid}`;
|
|
32
40
|
this.used.add(key);
|
|
33
41
|
let draw=this.draws.get(key);
|
|
34
42
|
if (!draw) {
|
|
@@ -37,6 +45,9 @@ export class BlenderObjectInfoMaterials {
|
|
|
37
45
|
draw={source,mesh,material,version:source.version};
|
|
38
46
|
this.draws.set(key,draw);
|
|
39
47
|
}
|
|
48
|
+
// Refresh follows a mesh that still wears this shared input, including
|
|
49
|
+
// when its former representative was edited or removed.
|
|
50
|
+
draw.mesh=mesh;
|
|
40
51
|
this.sync(draw,info);
|
|
41
52
|
return draw.material;
|
|
42
53
|
}
|
|
@@ -49,7 +60,11 @@ export class BlenderObjectInfoMaterials {
|
|
|
49
60
|
|
|
50
61
|
/** A pending authored graph can finish without another native export. */
|
|
51
62
|
needsRemap(): boolean {
|
|
52
|
-
|
|
63
|
+
for (const [source,key] of this.sources)
|
|
64
|
+
if ((materialGraph(source)?.key??null)!==key) return true;
|
|
65
|
+
for (const [mesh,value] of this.particleInputs)
|
|
66
|
+
if ((mesh.userData['blenderObjectInfo'] as BlenderObjectInfo|undefined)?.particle_random!==value) return true;
|
|
67
|
+
return false;
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
refresh(): void {
|
|
@@ -70,6 +85,6 @@ export class BlenderObjectInfoMaterials {
|
|
|
70
85
|
|
|
71
86
|
clear(): void {
|
|
72
87
|
for(const draw of this.draws.values()) draw.material.dispose();
|
|
73
|
-
this.draws.clear();this.sources.clear();this.used.clear();
|
|
88
|
+
this.draws.clear();this.sources.clear();this.used.clear();this.particleInputs.clear();
|
|
74
89
|
}
|
|
75
90
|
}
|
|
@@ -7,6 +7,7 @@ import {z} from 'zod';
|
|
|
7
7
|
import {applyGraphShader, bindGraphDraw, bindPaletteGraph, borrowGraphDrawBinding, graphProgramKey, graphUvChannels, setGraphViewport} from './blender-graph-material';
|
|
8
8
|
import type {CompiledGraph} from './blender-node-graph';
|
|
9
9
|
import {bindNamedUvChannels} from './blender-texture-samplers';
|
|
10
|
+
import {copyWorldVisibility,worldVisibilityShader,worldVisibilityUniforms} from './blender-world-visibility';
|
|
10
11
|
|
|
11
12
|
const scalar = z.number().finite();
|
|
12
13
|
const color = z.tuple([scalar, scalar, scalar]);
|
|
@@ -55,6 +56,7 @@ function graphShadow(material: THREE.MeshPhysicalMaterial): THREE.MeshPhysicalMa
|
|
|
55
56
|
// which `copy` replaces with the material's own (it leaves hooks alone).
|
|
56
57
|
applyPhysicalMaterial(shadow);
|
|
57
58
|
shadow.copy(material);
|
|
59
|
+
copyWorldVisibility(material,shadow);
|
|
58
60
|
const values = uniforms.get(shadow)!;
|
|
59
61
|
if (held) {
|
|
60
62
|
values.blenderCoatIor.value = held.blenderCoatIor.value;
|
|
@@ -149,6 +151,7 @@ export function syncPhysicalDrawMaterial(source: THREE.Material, target?: THREE.
|
|
|
149
151
|
return null;
|
|
150
152
|
}
|
|
151
153
|
copy.copy(source);
|
|
154
|
+
copyWorldVisibility(source,copy);
|
|
152
155
|
const held = uniforms.get(source)!;
|
|
153
156
|
const values = uniforms.get(copy)!;
|
|
154
157
|
values.blenderCoatIor.value = held.blenderCoatIor.value;
|
|
@@ -192,7 +195,7 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
|
|
|
192
195
|
blenderWorldExtinction: {value: new THREE.Vector3()}};
|
|
193
196
|
uniforms.set(material, values);
|
|
194
197
|
const held = values;
|
|
195
|
-
material.customProgramCacheKey = () => `blender-principled-physical-
|
|
198
|
+
material.customProgramCacheKey = () => `blender-principled-physical-v6${graphProgramKey(material)}`;
|
|
196
199
|
material.onBeforeRender = (renderer, scene, camera, geometry, object) => {
|
|
197
200
|
bindNamedUvChannels(material, geometry);
|
|
198
201
|
bindGraphDraw(material, geometry, renderer, scene, camera, object, graphShadow);
|
|
@@ -210,6 +213,12 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
|
|
|
210
213
|
.map(i => (i <= 3 ? `#ifndef USE_UV${i}\nattribute vec2 uv${i};\n#endif` : `attribute vec2 uv${i};`))
|
|
211
214
|
.join('\n') + '\n' + shader.vertexShader;
|
|
212
215
|
Object.assign(shader.uniforms, held);
|
|
216
|
+
worldVisibilityUniforms(material,shader.uniforms);
|
|
217
|
+
shader.vertexShader='varying vec3 blenderWorldPosition;\n'+shader.vertexShader;
|
|
218
|
+
shader.vertexShader=shader.vertexShader.replace('#include <worldpos_vertex>',
|
|
219
|
+
'#include <worldpos_vertex>\n#if defined(USE_ENVMAP) || defined(USE_SHADOWMAP) || defined(USE_TRANSMISSION)\nblenderWorldPosition=worldPosition.xyz;\n#else\nblenderWorldPosition=(modelMatrix*vec4(transformed,1.)).xyz;\n#endif');
|
|
220
|
+
shader.fragmentShader=shader.fragmentShader.replace('#include <envmap_physical_pars_fragment>',
|
|
221
|
+
'#include <envmap_physical_pars_fragment>\n'+worldVisibilityShader);
|
|
213
222
|
shader.fragmentShader = `uniform float blenderCoatIor;
|
|
214
223
|
uniform vec3 blenderWorldExtinction;
|
|
215
224
|
vec3 blenderInfiniteTransmission() { return vec3(
|
|
@@ -255,6 +264,10 @@ export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, inpu
|
|
|
255
264
|
'rectAreaLight = rectAreaLights[ i ]; rectAreaLight.color *= exp(-blenderWorldExtinction * length(rectAreaLight.position-geometryPosition));')
|
|
256
265
|
.replace('getAmbientLightIrradiance( ambientLightColor )', 'getAmbientLightIrradiance( ambientLightColor ) * blenderInfiniteTransmission()');
|
|
257
266
|
const environment = THREE.ShaderChunk.lights_fragment_maps
|
|
267
|
+
.replace('getIBLIrradiance( geometryNormal )',
|
|
268
|
+
'blenderWorldIrradiance( geometryNormal, getIBLIrradiance( geometryNormal ) )')
|
|
269
|
+
.replaceAll(/(getIBL(?:Radiance|AnisotropyRadiance)\([^;]+\))/g,
|
|
270
|
+
'$1 * blenderWorldReflectionVisibility(geometryViewDir,geometryNormal,material.roughness)')
|
|
258
271
|
.replaceAll(/(getIBL(?:Irradiance|Radiance|AnisotropyRadiance)\([^;]+\))/g, '$1 * blenderInfiniteTransmission()');
|
|
259
272
|
shader.fragmentShader = shader.fragmentShader.replace('#include <lights_physical_fragment>', physical)
|
|
260
273
|
.replace('#include <lights_fragment_begin>', lighting)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import type {RenderRequest} from '../protocol';
|
|
3
|
+
|
|
4
|
+
/** Render through the authored camera, including its deliberate clipping.
|
|
5
|
+
* Navigation's automatically fitted clipping does not describe a render. */
|
|
6
|
+
export function blenderRenderCamera(render: RenderRequest, documentScale = 1): THREE.PerspectiveCamera | THREE.OrthographicCamera {
|
|
7
|
+
const aspect = render.width / render.height;
|
|
8
|
+
const near = (render.clipStart ?? .1) * documentScale;
|
|
9
|
+
const far = (render.clipEnd ?? 2000) * documentScale;
|
|
10
|
+
const camera = render.orthographic
|
|
11
|
+
? new THREE.OrthographicCamera(-render.fov * documentScale * aspect / 2,
|
|
12
|
+
render.fov * documentScale * aspect / 2, render.fov * documentScale / 2,
|
|
13
|
+
-render.fov * documentScale / 2, near, far)
|
|
14
|
+
: new THREE.PerspectiveCamera(render.fov, aspect, near, far);
|
|
15
|
+
// Blender's shift uses the fitted sensor dimension for BOTH axes.
|
|
16
|
+
// View offset survives projection updates and capture camera cloning.
|
|
17
|
+
const fitted = (render.horizontalFit ?? aspect >= 1) ? render.width : render.height;
|
|
18
|
+
camera.setViewOffset(render.width, render.height,
|
|
19
|
+
(render.shiftX ?? 0) * fitted, -(render.shiftY ?? 0) * fitted,
|
|
20
|
+
render.width, render.height);
|
|
21
|
+
return camera;
|
|
22
|
+
}
|
|
@@ -47,6 +47,8 @@ import {
|
|
|
47
47
|
sampleWorldScreen,
|
|
48
48
|
usesWindowCoordinates,
|
|
49
49
|
worldField,
|
|
50
|
+
worldSolarIrradiance,
|
|
51
|
+
withoutSolarDiscs,
|
|
50
52
|
} from './world-field-sampler';
|
|
51
53
|
import { type WorldMathOperation, worldMath } from './world-math';
|
|
52
54
|
|
|
@@ -436,6 +438,9 @@ export type WorldExpression =
|
|
|
436
438
|
| {
|
|
437
439
|
kind: 'sky';
|
|
438
440
|
sky_model?: 'SINGLE_SCATTERING' | 'MULTIPLE_SCATTERING' | undefined;
|
|
441
|
+
sun_disc?: boolean | undefined;
|
|
442
|
+
sun_size?: number | undefined;
|
|
443
|
+
sun_intensity?: number | undefined;
|
|
439
444
|
sun_elevation: number;
|
|
440
445
|
sun_rotation: number;
|
|
441
446
|
altitude: number;
|
|
@@ -533,6 +538,9 @@ const worldExpression: z.ZodType<WorldExpression> = z.lazy(() =>
|
|
|
533
538
|
.object({
|
|
534
539
|
kind: z.literal('sky'),
|
|
535
540
|
sky_model: z.enum(['SINGLE_SCATTERING', 'MULTIPLE_SCATTERING']).optional(),
|
|
541
|
+
sun_disc: z.boolean().optional(),
|
|
542
|
+
sun_size: scalar.optional(),
|
|
543
|
+
sun_intensity: scalar.optional(),
|
|
536
544
|
sun_elevation: scalar,
|
|
537
545
|
sun_rotation: scalar,
|
|
538
546
|
altitude: scalar,
|
|
@@ -690,9 +698,18 @@ function worldDataTexture(
|
|
|
690
698
|
width: number,
|
|
691
699
|
height: number,
|
|
692
700
|
): THREE.DataTexture {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
701
|
+
// Solar radiance can exceed half-float's 65504 limit even at native default
|
|
702
|
+
// intensity. Keep those backgrounds in float32 instead of clipping the field.
|
|
703
|
+
const fullFloat = samples.some(value => Math.abs(value * strength) > 65504);
|
|
704
|
+
const data = fullFloat ? new Float32Array(width * height * 4) : new Uint16Array(width * height * 4);
|
|
705
|
+
if (data instanceof Float32Array) {
|
|
706
|
+
for (let i = 0; i < width * height; i++) {
|
|
707
|
+
for (let c = 0; c < 3; c++) data[i * 4 + c] = samples[i * 3 + c]! * strength;
|
|
708
|
+
data[i * 4 + 3] = 1;
|
|
709
|
+
}
|
|
710
|
+
} else writeScaled(data, samples, strength, width * height);
|
|
711
|
+
const texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat,
|
|
712
|
+
fullFloat ? THREE.FloatType : THREE.HalfFloatType);
|
|
696
713
|
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
697
714
|
texture.colorSpace = THREE.LinearSRGBColorSpace;
|
|
698
715
|
texture.minFilter = THREE.LinearFilter;
|
|
@@ -707,6 +724,14 @@ export type WorldData = z.infer<typeof worldSchema>;
|
|
|
707
724
|
* Constants and linked colors use the same radiometric path. The environment
|
|
708
725
|
* samples the field at 256×128; features below a texel are filtered. */
|
|
709
726
|
export class WorldBackground {
|
|
727
|
+
lightingTexture():THREE.Texture|null {return this.environmentTexture??this.texture;}
|
|
728
|
+
readonly solarLights: THREE.DirectionalLight[] = [];
|
|
729
|
+
private readonly solarDirections = new Map<THREE.DirectionalLight, THREE.Vector3>();
|
|
730
|
+
|
|
731
|
+
updateSolarDirections(root: THREE.Object3D): void {
|
|
732
|
+
for (const [light, direction] of this.solarDirections)
|
|
733
|
+
light.position.copy(direction).transformDirection(root.matrixWorld);
|
|
734
|
+
}
|
|
710
735
|
private pending: Promise<void> | null = null;
|
|
711
736
|
|
|
712
737
|
/** Await this world's preparation, including a failure that settled before
|
|
@@ -772,7 +797,7 @@ export class WorldBackground {
|
|
|
772
797
|
];
|
|
773
798
|
const missing = skies.filter((parameters) => !hasSkyTexture(parameters));
|
|
774
799
|
if (missing.length === 0) {
|
|
775
|
-
this.compose(scene, world, expression, camera);
|
|
800
|
+
this.compose(scene, world, expression, camera, root);
|
|
776
801
|
return;
|
|
777
802
|
}
|
|
778
803
|
const pending = (async () => {
|
|
@@ -783,7 +808,7 @@ export class WorldBackground {
|
|
|
783
808
|
// The world moved on (another apply, or a clear) while the worker ran:
|
|
784
809
|
// composing now would paint a sky nobody asked for over the current one.
|
|
785
810
|
if (this.generation !== generation || this.applied?.scene !== scene) return;
|
|
786
|
-
this.compose(scene, world, expression, camera);
|
|
811
|
+
this.compose(scene, world, expression, camera, root);
|
|
787
812
|
presenterChanged();
|
|
788
813
|
})();
|
|
789
814
|
this.pending = pending;
|
|
@@ -802,6 +827,7 @@ export class WorldBackground {
|
|
|
802
827
|
world: WorldData,
|
|
803
828
|
expression: WorldExpression,
|
|
804
829
|
camera?: THREE.Camera,
|
|
830
|
+
root?: THREE.Object3D,
|
|
805
831
|
): void {
|
|
806
832
|
this.texture = worldTexture(expression, world.strength, camera);
|
|
807
833
|
if (usesWindowCoordinates(expression)) {
|
|
@@ -833,14 +859,37 @@ export class WorldBackground {
|
|
|
833
859
|
// 0.30 behind a 0.95 backdrop -- and three keeps the two apart, so using
|
|
834
860
|
// the backdrop here would light that scene three times too brightly.
|
|
835
861
|
const { lighting } = world;
|
|
836
|
-
|
|
837
|
-
|
|
862
|
+
const lightingExpression = lighting?.shader ?? lighting?.color ?? expression;
|
|
863
|
+
const solar = worldSolarIrradiance(lightingExpression);
|
|
864
|
+
this.environmentTexture = lighting || solar.length
|
|
865
|
+
? worldTexture(solar.length ? withoutSolarDiscs(lightingExpression) : lightingExpression,
|
|
866
|
+
lighting?.strength ?? world.strength, camera)
|
|
838
867
|
: null;
|
|
839
868
|
scene.environment = this.environmentTexture ?? this.texture;
|
|
869
|
+
root?.updateMatrixWorld(true);
|
|
870
|
+
for (const {direction, irradiance} of solar) {
|
|
871
|
+
const light = new THREE.DirectionalLight(0xffffff, lighting?.strength ?? world.strength);
|
|
872
|
+
light.name = 'BlenderWorldSun';
|
|
873
|
+
light.color.setRGB(irradiance.x, irradiance.y, irradiance.z, THREE.LinearSRGBColorSpace);
|
|
874
|
+
light.position.copy(direction);
|
|
875
|
+
if (root) light.position.transformDirection(root.matrixWorld);
|
|
876
|
+
light.castShadow = true;
|
|
877
|
+
light.shadow.mapSize.set(2048, 2048);
|
|
878
|
+
scene.add(light, light.target);
|
|
879
|
+
this.solarLights.push(light);
|
|
880
|
+
this.solarDirections.set(light, direction.clone());
|
|
881
|
+
}
|
|
840
882
|
}
|
|
841
883
|
|
|
842
884
|
clear(): void {
|
|
843
885
|
this.generation++;
|
|
886
|
+
for (const light of this.solarLights) {
|
|
887
|
+
light.removeFromParent();
|
|
888
|
+
light.target.removeFromParent();
|
|
889
|
+
light.dispose();
|
|
890
|
+
}
|
|
891
|
+
this.solarLights.length = 0;
|
|
892
|
+
this.solarDirections.clear();
|
|
844
893
|
if (!this.applied) return;
|
|
845
894
|
this.applied.scene.background = this.applied.background;
|
|
846
895
|
this.applied.scene.environment = this.applied.environment;
|
|
@@ -58,6 +58,11 @@ export function fitModelDirectionalShadow(
|
|
|
58
58
|
casters: THREE.Box3[],
|
|
59
59
|
): void {
|
|
60
60
|
if (!light.castShadow || !receivers.length) return;
|
|
61
|
+
// A detached photograph fits before its first render. Newly composed suns
|
|
62
|
+
// and their targets have not yet passed through Scene.updateMatrixWorld;
|
|
63
|
+
// fitting in those stale axes produces a different map on the first draw.
|
|
64
|
+
light.updateWorldMatrix(true, false);
|
|
65
|
+
light.target.updateWorldMatrix(true, false);
|
|
61
66
|
light.shadow.updateMatrices(light);
|
|
62
67
|
const camera = light.shadow.camera;
|
|
63
68
|
const view = camera.matrixWorldInverse;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* No modeling operations or source builds run here. Stable datablock addresses
|
|
3
3
|
* retain objects/resources; changed meshes replace only their draw geometry.
|
|
4
4
|
*/
|
|
5
|
+
import {expandDrawPlacements} from './blender-depsgraph-placements';
|
|
5
6
|
import * as THREE from 'three';
|
|
6
7
|
import { z } from 'zod';
|
|
7
8
|
import { onPresenterChange, presenterChanged } from './blender-presenter-change';
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
worldSchema,
|
|
26
27
|
} from './blender-runtime-lighting';
|
|
27
28
|
import { fitModelDirectionalShadow, visibleShadowReceivers } from './blender-runtime-shadows';
|
|
29
|
+
import {BlenderWorldVisibility,bindWorldVisibility} from './blender-world-visibility';
|
|
28
30
|
import { volumeMesh, volumeSchema } from './blender-runtime-volume';
|
|
29
31
|
import { applyPhysicalMaterial, applyWorldExtinction, physicalMaterialSchema } from './blender-physical-material';
|
|
30
32
|
import { prepareGraphGeometry, setMaterialGraph } from './blender-graph-material';
|
|
@@ -490,6 +492,13 @@ export const frameSchema = z
|
|
|
490
492
|
meshes: z.record(z.string(), meshSchema),
|
|
491
493
|
materials: z.record(z.string(), materialSchema),
|
|
492
494
|
images: z.record(z.string(), frameImageSchema).default({}),
|
|
495
|
+
/** Evaluated draws borrowing geometry/materials from native source rows. */
|
|
496
|
+
instances: z.array(z.object({
|
|
497
|
+
id:z.string(),source:z.string(),owner:z.string().nullable(),
|
|
498
|
+
matrix:z.array(z.tuple([scalar,scalar,scalar,scalar])).length(4),
|
|
499
|
+
visible:z.boolean(),render_visible:z.boolean(),random:scalar,color:z.tuple([scalar,scalar,scalar,scalar]),
|
|
500
|
+
shadow_visible:z.boolean().optional(),particle_random:scalar.optional(),
|
|
501
|
+
}).strict()).optional(),
|
|
493
502
|
objects: z.array(
|
|
494
503
|
z
|
|
495
504
|
.object({
|
|
@@ -519,15 +528,19 @@ export const frameSchema = z
|
|
|
519
528
|
matrix: z.array(z.tuple([scalar, scalar, scalar, scalar])).length(4),
|
|
520
529
|
visible: z.boolean(),
|
|
521
530
|
render_visible: z.boolean().default(true),
|
|
531
|
+
shadow_visible: z.boolean().default(true),
|
|
522
532
|
selected: z.boolean(),
|
|
523
533
|
parent: z.string().nullable(),
|
|
534
|
+
instance_owner: z.string().nullable().optional(),
|
|
535
|
+
viewport_show_self:z.boolean().optional(),
|
|
536
|
+
render_show_self:z.boolean().optional(),
|
|
524
537
|
/** A manual texture space (`[location, size]`), which Generated
|
|
525
538
|
* coordinates map through; absent means Blender's automatic one,
|
|
526
539
|
* the evaluated bounds (`blender-graph-material.ts`'s orco). */
|
|
527
540
|
texspace: z.tuple([z.tuple([scalar, scalar, scalar]), z.tuple([scalar, scalar, scalar])]).optional(),
|
|
528
541
|
default_color: z.string().optional(),
|
|
529
542
|
object_info: z.object({
|
|
530
|
-
color:z.tuple([scalar,scalar,scalar,scalar]),index:scalar,random:scalar,
|
|
543
|
+
color:z.tuple([scalar,scalar,scalar,scalar]),index:scalar,random:scalar,particle_random:scalar.optional(),
|
|
531
544
|
}).strict().optional(),
|
|
532
545
|
})
|
|
533
546
|
.strict(),
|
|
@@ -669,6 +682,7 @@ export class BlenderRuntimeView {
|
|
|
669
682
|
private readonly lights = new Map<string, THREE.Light>();
|
|
670
683
|
private readonly lighting = new ViewportLighting();
|
|
671
684
|
private readonly world = new WorldBackground();
|
|
685
|
+
private readonly worldVisibility=new BlenderWorldVisibility();
|
|
672
686
|
/**
|
|
673
687
|
* THE INSPECTION OVERLAYS (I4). They are NOT children of {@link root}, and
|
|
674
688
|
* that is the whole design: an overlay is EDITOR FURNITURE, so its
|
|
@@ -701,10 +715,12 @@ export class BlenderRuntimeView {
|
|
|
701
715
|
private readonly emptyExtrasRoot = new THREE.Group();
|
|
702
716
|
private rendered = false;
|
|
703
717
|
private sceneLights = false;
|
|
718
|
+
private readonly hiddenSurfaceLayers = new WeakMap<THREE.Object3D,number>();
|
|
704
719
|
private shadowFit: {
|
|
705
720
|
camera: THREE.Camera | undefined;
|
|
706
721
|
cameraMatrix: THREE.Matrix4 | undefined;
|
|
707
722
|
projection: THREE.Matrix4 | undefined;
|
|
723
|
+
solarLights: THREE.DirectionalLight[];
|
|
708
724
|
objects: Map<THREE.Object3D, { matrix: THREE.Matrix4; visible: boolean }>;
|
|
709
725
|
} | null = null;
|
|
710
726
|
/** The camera the VIEWPORT is held in render lighting through (Blender's Rendered shading),
|
|
@@ -723,7 +739,7 @@ export class BlenderRuntimeView {
|
|
|
723
739
|
* (`blender-workbench-material.ts`) over their viewport display. */
|
|
724
740
|
private workbench = false;
|
|
725
741
|
private readonly workbenchMaterials = new Map<string, THREE.Material>();
|
|
726
|
-
private readonly fallback = new THREE.MeshPhysicalMaterial({ color: 0xb9bec6, roughness: 0.72 });
|
|
742
|
+
private readonly fallback = new THREE.MeshPhysicalMaterial({ color: 0xb9bec6, roughness: 0.72, side: THREE.DoubleSide });
|
|
727
743
|
/** Base Color images, by image name -- ONE texture per image however many
|
|
728
744
|
* materials read it, and the cache OWNS it: a material points at one and
|
|
729
745
|
* never disposes it. Held with the size and revision the resident bytes
|
|
@@ -1319,6 +1335,13 @@ export class BlenderRuntimeView {
|
|
|
1319
1335
|
// objects. Fit shadows for that draw, rather than the camera's pose
|
|
1320
1336
|
// when Rendered shading was first entered. Captures use this path too.
|
|
1321
1337
|
if (this.rendered) this.applyShadows(true, camera);
|
|
1338
|
+
this.worldVisibility.sync(this.objects.values(),this.world.lightingTexture(),this.root,this.rendered);
|
|
1339
|
+
for(const material of [...this.materials.values(),this.fallback])bindWorldVisibility(material,this.worldVisibility);
|
|
1340
|
+
for(const object of this.objects.values()) {
|
|
1341
|
+
const mesh=object as THREE.Mesh;if(!mesh.isMesh)continue;
|
|
1342
|
+
for(const material of Array.isArray(mesh.material)?mesh.material:[mesh.material])bindWorldVisibility(material,this.worldVisibility);
|
|
1343
|
+
}
|
|
1344
|
+
if(options?.renderer)this.worldVisibility.prepare(options.renderer);
|
|
1322
1345
|
const opaqueDone = performance.now();
|
|
1323
1346
|
const motion = this.motionGeometry.prepare(camera, !this.rendered && (this.mode ?? this.frame?.mode) === 'OBJECT' &&
|
|
1324
1347
|
options?.interactive === true, options?.height ?? 0);
|
|
@@ -1418,6 +1441,9 @@ export class BlenderRuntimeView {
|
|
|
1418
1441
|
await lightingReady();
|
|
1419
1442
|
await Promise.all([...this.textures.values()].map((held) => held.ready));
|
|
1420
1443
|
await this.world.ready();
|
|
1444
|
+
// The asynchronous sky composition creates solar lights. Fit their
|
|
1445
|
+
// shadows only after they exist, including the first detached capture.
|
|
1446
|
+
this.applyShadows(rendered, camera);
|
|
1421
1447
|
}
|
|
1422
1448
|
}
|
|
1423
1449
|
|
|
@@ -1440,9 +1466,12 @@ export class BlenderRuntimeView {
|
|
|
1440
1466
|
*/
|
|
1441
1467
|
private applyShadows(rendered: boolean, camera?: THREE.Camera): void {
|
|
1442
1468
|
this.root.updateMatrixWorld(true);
|
|
1469
|
+
this.world.updateSolarDirections(this.root);
|
|
1443
1470
|
const fitted = this.shadowFit;
|
|
1444
1471
|
if (rendered && fitted && fitted.camera === camera &&
|
|
1445
1472
|
(!camera || (fitted.cameraMatrix?.equals(camera.matrixWorld) && fitted.projection?.equals(camera.projectionMatrix))) &&
|
|
1473
|
+
fitted.solarLights.length === this.world.solarLights.length &&
|
|
1474
|
+
fitted.solarLights.every((light, index) => light === this.world.solarLights[index]) &&
|
|
1446
1475
|
fitted.objects.size === this.objects.size &&
|
|
1447
1476
|
[...this.objects.values()].every(object => {
|
|
1448
1477
|
const previous = fitted.objects.get(object);
|
|
@@ -1452,18 +1481,22 @@ export class BlenderRuntimeView {
|
|
|
1452
1481
|
camera,
|
|
1453
1482
|
cameraMatrix: camera?.matrixWorld.clone(),
|
|
1454
1483
|
projection: camera?.projectionMatrix.clone(),
|
|
1484
|
+
solarLights: [...this.world.solarLights],
|
|
1455
1485
|
objects: new Map([...this.objects.values()].map(object =>
|
|
1456
1486
|
[object, { matrix: object.matrixWorld.clone(), visible: object.visible }])),
|
|
1457
1487
|
} : null;
|
|
1458
1488
|
const boxes: THREE.Box3[] = [];
|
|
1459
|
-
|
|
1489
|
+
const casters: THREE.Box3[] = [];
|
|
1490
|
+
const shadowVisibility = new Map(this.frame?.objects.map(row => [row.id, row.shadow_visible]) ?? []);
|
|
1491
|
+
for (const [id, object] of this.objects) {
|
|
1460
1492
|
const mesh = object as THREE.Mesh;
|
|
1461
1493
|
if (!mesh.isMesh) continue;
|
|
1462
|
-
mesh.castShadow = rendered;
|
|
1494
|
+
mesh.castShadow = rendered && shadowVisibility.get(id) !== false;
|
|
1463
1495
|
mesh.receiveShadow = rendered;
|
|
1464
1496
|
if (rendered && mesh.visible) {
|
|
1465
1497
|
const bounds = new THREE.Box3().setFromObject(mesh);
|
|
1466
1498
|
if (!bounds.isEmpty()) boxes.push(bounds);
|
|
1499
|
+
if (!bounds.isEmpty() && mesh.castShadow) casters.push(bounds);
|
|
1467
1500
|
}
|
|
1468
1501
|
}
|
|
1469
1502
|
if (!rendered || !boxes.length) return;
|
|
@@ -1477,9 +1510,9 @@ export class BlenderRuntimeView {
|
|
|
1477
1510
|
const receivers = frustum
|
|
1478
1511
|
? boxes.flatMap((bounds) => visibleShadowReceivers(bounds, frustum))
|
|
1479
1512
|
: [];
|
|
1480
|
-
for (const light of this.lights.values()) {
|
|
1513
|
+
for (const light of [...this.lights.values(), ...this.world.solarLights]) {
|
|
1481
1514
|
if ((light as THREE.DirectionalLight).isDirectionalLight && camera)
|
|
1482
|
-
fitModelDirectionalShadow(light as THREE.DirectionalLight, receivers,
|
|
1515
|
+
fitModelDirectionalShadow(light as THREE.DirectionalLight, receivers, casters);
|
|
1483
1516
|
else fitShadow(light, sphere.center, sphere.radius);
|
|
1484
1517
|
}
|
|
1485
1518
|
}
|
|
@@ -1580,11 +1613,26 @@ export class BlenderRuntimeView {
|
|
|
1580
1613
|
}
|
|
1581
1614
|
|
|
1582
1615
|
private applyVisibility(): void {
|
|
1616
|
+
// Restore canonical masks before suppressing an instancer's own surface.
|
|
1617
|
+
// Its geometry can be hidden while its placed children remain visible.
|
|
1618
|
+
this.instances.clear();
|
|
1619
|
+
this.transparentInstances.clear();
|
|
1583
1620
|
const renderVisibility = this.rendered && (this.capturing || this.heldVisibility === 'render');
|
|
1584
1621
|
for (const obj of this.frame?.objects ?? []) {
|
|
1585
1622
|
const object = this.objects.get(obj.id);
|
|
1586
1623
|
const visible = renderVisibility ? obj.render_visible : obj.visible;
|
|
1587
1624
|
if (object) object.visible = visible;
|
|
1625
|
+
if ((object as THREE.Mesh | undefined)?.isMesh) {
|
|
1626
|
+
const showSelf = (renderVisibility ? obj.render_show_self : obj.viewport_show_self) ?? true;
|
|
1627
|
+
const previous = this.hiddenSurfaceLayers.get(object!);
|
|
1628
|
+
if (!showSelf) {
|
|
1629
|
+
if (previous === undefined) this.hiddenSurfaceLayers.set(object!,object!.layers.mask);
|
|
1630
|
+
object!.layers.mask = 0;
|
|
1631
|
+
} else if (previous !== undefined) {
|
|
1632
|
+
object!.layers.mask = previous;
|
|
1633
|
+
this.hiddenSurfaceLayers.delete(object!);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1588
1636
|
if (!obj.light) continue;
|
|
1589
1637
|
const light = this.lights.get(obj.light);
|
|
1590
1638
|
if (light) light.visible = this.sceneLights && visible;
|
|
@@ -1646,7 +1694,7 @@ export class BlenderRuntimeView {
|
|
|
1646
1694
|
// the session correct its own tables BEFORE it decides what to ship.
|
|
1647
1695
|
const held =
|
|
1648
1696
|
this.frame === null ? null : { session: this.frame.session, revision: this.frame.revision };
|
|
1649
|
-
const next = frameSchema.parse(input);
|
|
1697
|
+
const next = expandDrawPlacements(frameSchema.parse(input));
|
|
1650
1698
|
const staged = this.staged;
|
|
1651
1699
|
if (staged && (staged.session !== next.session || staged.revision !== next.revision))
|
|
1652
1700
|
throw new Error(`Blender frame manifest does not match its staged revision: staged ${JSON.stringify([staged.session, staged.revision])}, manifest ${JSON.stringify([next.session, next.revision])}`);
|
|
@@ -1849,7 +1897,9 @@ export class BlenderRuntimeView {
|
|
|
1849
1897
|
});
|
|
1850
1898
|
}
|
|
1851
1899
|
for (const [id, data] of Object.entries(next.materials)) {
|
|
1852
|
-
|
|
1900
|
+
// Cycles intersects and shades both faces, including an arcade viewed
|
|
1901
|
+
// from underneath. Three's front-face default drops those surfaces.
|
|
1902
|
+
const material = this.materials.get(id) ?? new THREE.MeshPhysicalMaterial({ side: THREE.DoubleSide });
|
|
1853
1903
|
material.name = data.name;
|
|
1854
1904
|
// A LINKED BASE COLOUR REPLACES THE SOCKET'S VALUE, it does not multiply
|
|
1855
1905
|
// it. Python's `_reduce` fills `color` from the Base Color socket's
|
|
@@ -2181,7 +2231,7 @@ export class BlenderRuntimeView {
|
|
|
2181
2231
|
}
|
|
2182
2232
|
|
|
2183
2233
|
drawStatistics() {
|
|
2184
|
-
return {enabled: this.drawBatching, preparation: {...this.drawPreparation}, materialRanges: this.materialRanges.inspect(), opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
|
|
2234
|
+
return {enabled: this.drawBatching, preparation: {...this.drawPreparation}, worldVisibility:this.worldVisibility.diagnostics(), materialRanges: this.materialRanges.inspect(), opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
|
|
2185
2235
|
}
|
|
2186
2236
|
|
|
2187
2237
|
snapshot() {
|
|
@@ -2363,6 +2413,10 @@ export class BlenderRuntimeView {
|
|
|
2363
2413
|
if (disposed) throw new Error('Blender capture snapshot is disposed');
|
|
2364
2414
|
return detached.setRendered(true, camera);
|
|
2365
2415
|
},
|
|
2416
|
+
prepareDraw:(renderer:THREE.WebGLRenderer,camera:THREE.Camera)=>{
|
|
2417
|
+
if(disposed)throw new Error('Blender capture snapshot is disposed');
|
|
2418
|
+
return detached.prepareDraw(camera,{interactive:false,height:renderer.domElement.height,renderer});
|
|
2419
|
+
},
|
|
2366
2420
|
dispose,
|
|
2367
2421
|
};
|
|
2368
2422
|
}
|
|
@@ -2411,6 +2465,7 @@ export class BlenderRuntimeView {
|
|
|
2411
2465
|
}
|
|
2412
2466
|
|
|
2413
2467
|
private clear() {
|
|
2468
|
+
this.worldVisibility.dispose();
|
|
2414
2469
|
this.objectInfoMaterials.clear();
|
|
2415
2470
|
this.materialRanges.clear();
|
|
2416
2471
|
this.motionGeometry.clear();
|
|
@@ -71,6 +71,26 @@ function spectrumToXyz(spectrum: Float32Array): V {
|
|
|
71
71
|
return [f(x * 20), f(y * 20), f(z * 20)];
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** SKY_single_scattering_precompute_sun: native spectral radiance at the
|
|
75
|
+
* bottom and top of the solar disc, before intensity and limb darkening. */
|
|
76
|
+
export function precomputeSingleScatteringSun(p: SkyParameters, angularDiameter: number): [V, V] {
|
|
77
|
+
const diameter = f(angularDiameter), half = f(diameter / 2);
|
|
78
|
+
const solidAngle = f(TWO_PI * f(1 - f(Math.cos(half))));
|
|
79
|
+
if (!(solidAngle > 0) || f(p.sunElevation + half) <= 0) return [[0, 0, 0], [0, 0, 0]];
|
|
80
|
+
const origin: V = [0, 0, f(EARTH_RADIUS + Math.min(59999, Math.max(1, f(p.altitude))))];
|
|
81
|
+
const pixel = (elevation: number): V => {
|
|
82
|
+
const depth = opticalDepth(origin, direction(Math.max(0, elevation), 0));
|
|
83
|
+
const spectrum = new Float32Array(21);
|
|
84
|
+
for (let i = 0; i < 21; i++) {
|
|
85
|
+
const extinction = f(f(f(RAYLEIGH_COEFF[i]! * depth[0]) * f(p.airDensity)) +
|
|
86
|
+
f(f(f(f(1.11 * MIE_COEFF) * depth[1])) * f(p.aerosolDensity)));
|
|
87
|
+
spectrum[i] = f(f(IRRADIANCE[i]! * f(Math.exp(-extinction))) / solidAngle);
|
|
88
|
+
}
|
|
89
|
+
return spectrumToXyz(spectrum);
|
|
90
|
+
};
|
|
91
|
+
return [pixel(f(f(p.sunElevation) - half)), pixel(f(f(p.sunElevation) + half))];
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
function inscattering(ray: V, sun: V, origin: V, factors: V): V {
|
|
75
95
|
const end = add(origin, scale(ray, atmosphereDistance(origin, ray)));
|
|
76
96
|
const rayLength = length([f(end[0] - origin[0]), f(end[1] - origin[1]), f(end[2] - origin[2])]);
|