@volter/blender-engine 0.1.126 → 0.1.128
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 +32 -13
- package/browser/blender-engine.mts +2 -2
- package/browser/blender-wali-engine.mts +1 -1
- package/browser/protocol.ts +2 -0
- package/browser/runtime.ts +1 -1
- package/browser/session.py +65 -17
- package/browser/three/blender-display-NOTICE.txt +45 -0
- package/browser/three/blender-display-shaders.json +1 -0
- package/browser/three/blender-display-shaders.lut +0 -0
- package/browser/three/blender-display-transform.ts +139 -0
- package/browser/three/blender-graph-material.ts +34 -7
- package/browser/three/blender-node-graph.ts +15 -1
- package/browser/three/blender-object-info-materials.ts +75 -0
- package/browser/three/blender-physical-material.ts +13 -2
- package/browser/three/blender-runtime-armature.ts +1 -1
- package/browser/three/blender-runtime-lighting.ts +3 -1
- package/browser/three/blender-runtime-view.ts +16 -1
- package/browser/three/blender-single-scattering-sky.ts +128 -0
- package/browser/three/blender-sky.ts +19 -15
- package/browser/three/presenter.ts +8 -55
- package/browser/three/world-field-sampler.ts +2 -0
- package/browser/volter_three.py +4 -5
- package/browser/worker.ts +45 -12
- package/package.json +1 -1
|
Binary file
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
|
|
3
|
+
import displayShaders from './blender-display-shaders.json';
|
|
4
|
+
import type { RenderRequest } from '../protocol';
|
|
5
|
+
|
|
6
|
+
export interface BlenderDisplaySettings {
|
|
7
|
+
readonly transform: string;
|
|
8
|
+
readonly look: string;
|
|
9
|
+
/** Linear multiplier, after alpha unassociation and before the view transform. */
|
|
10
|
+
readonly exposure: number;
|
|
11
|
+
readonly gamma: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function blenderDisplaySettingsForRender(render: Pick<RenderRequest, 'toneMapping' | 'look' | 'exposure' | 'gamma'>): BlenderDisplaySettings {
|
|
15
|
+
return {
|
|
16
|
+
transform: {none: 'Standard', filmic: 'Filmic', agx: 'AgX', neutral: 'Khronos PBR Neutral'}[render.toneMapping],
|
|
17
|
+
look: render.look ?? 'None', exposure: render.exposure, gamma: render.gamma ?? 1,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DisplayTexture {
|
|
22
|
+
readonly sampler: string;
|
|
23
|
+
readonly offset: number;
|
|
24
|
+
readonly count: number;
|
|
25
|
+
readonly linear: boolean;
|
|
26
|
+
readonly channels: number;
|
|
27
|
+
readonly size?: number;
|
|
28
|
+
readonly width?: number;
|
|
29
|
+
readonly height?: number;
|
|
30
|
+
}
|
|
31
|
+
interface DisplayProcessor {
|
|
32
|
+
readonly shader: string;
|
|
33
|
+
readonly textures: readonly DisplayTexture[];
|
|
34
|
+
}
|
|
35
|
+
const processors: Readonly<Record<string, DisplayProcessor>> = displayShaders.processors;
|
|
36
|
+
|
|
37
|
+
let tableData: Promise<ArrayBuffer> | null = null;
|
|
38
|
+
function loadDisplayData(): Promise<ArrayBuffer> {
|
|
39
|
+
return tableData ??= fetch(new URL('./blender-display-shaders.lut', import.meta.url))
|
|
40
|
+
.then(async response => {
|
|
41
|
+
if (!response.ok) throw new Error(`Blender display tables: ${response.status}`);
|
|
42
|
+
return response.arrayBuffer();
|
|
43
|
+
}).catch(error => { tableData = null; throw error; });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Native OCIO-generated shaders, not a resampled look+view cube. The exact
|
|
47
|
+
* operation order and tetrahedral sampling come from Blender's pinned config.
|
|
48
|
+
* Apply after scene-linear compositing; exposure precedes the view, gamma
|
|
49
|
+
* follows it. Resources belong to the mounted document, never the frame loop. */
|
|
50
|
+
export class BlenderDisplayTransform {
|
|
51
|
+
private readonly material: THREE.RawShaderMaterial;
|
|
52
|
+
private readonly quad: FullScreenQuad;
|
|
53
|
+
private readonly textures: THREE.Texture[] = [];
|
|
54
|
+
|
|
55
|
+
constructor(settings: BlenderDisplaySettings, data: ArrayBuffer) {
|
|
56
|
+
const processor = processors[`${settings.transform}/${settings.look}`];
|
|
57
|
+
if (!processor) throw new Error(`Unimplemented Blender display transform: ${settings.transform}, ${settings.look}`);
|
|
58
|
+
const uniforms: Record<string, THREE.IUniform> = {
|
|
59
|
+
frame: {value: null}, exposure: {value: settings.exposure},
|
|
60
|
+
exponent: {value: 1 / Math.max(1e-7, settings.gamma)}, straightAlpha: {value: false},
|
|
61
|
+
};
|
|
62
|
+
for (const spec of processor.textures) {
|
|
63
|
+
if (spec.offset + spec.count * 4 > data.byteLength)
|
|
64
|
+
throw new Error('Invalid Blender display table');
|
|
65
|
+
}
|
|
66
|
+
for (const spec of processor.textures) {
|
|
67
|
+
const values = new Float32Array(data, spec.offset, spec.count);
|
|
68
|
+
// OCIO stores a 3D texture with blue varying fastest. Its generated GLSL
|
|
69
|
+
// samples .zyx, so retain that order rather than transposing the cube.
|
|
70
|
+
let texture: THREE.DataTexture | THREE.Data3DTexture;
|
|
71
|
+
if (spec.size) texture = new THREE.Data3DTexture(values, spec.size, spec.size, spec.size);
|
|
72
|
+
else texture = new THREE.DataTexture(values, spec.width, spec.height);
|
|
73
|
+
texture.type = THREE.FloatType;
|
|
74
|
+
texture.format = spec.channels === 1 ? THREE.RedFormat : THREE.RGBFormat;
|
|
75
|
+
// Three infers sized float formats for Red/RGBA, but not RGB. WebGL2's
|
|
76
|
+
// immutable 3D storage rejects an unsized RGB internal format.
|
|
77
|
+
texture.internalFormat = spec.channels === 1 ? 'R32F' : 'RGB32F';
|
|
78
|
+
texture.minFilter = texture.magFilter = spec.linear ? THREE.LinearFilter : THREE.NearestFilter;
|
|
79
|
+
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
80
|
+
texture.unpackAlignment = 1;
|
|
81
|
+
texture.needsUpdate = true;
|
|
82
|
+
this.textures.push(texture);
|
|
83
|
+
uniforms[spec.sampler] = {value: texture};
|
|
84
|
+
}
|
|
85
|
+
this.material = new THREE.RawShaderMaterial({
|
|
86
|
+
name: 'Blender OCIO display', glslVersion: THREE.GLSL3,
|
|
87
|
+
depthTest: false, depthWrite: false, blending: THREE.NoBlending,
|
|
88
|
+
uniforms,
|
|
89
|
+
vertexShader: `precision highp float; in vec3 position; in vec2 uv; out vec2 vUv;
|
|
90
|
+
void main(){vUv=uv;gl_Position=vec4(position,1.0);}`,
|
|
91
|
+
fragmentShader: `precision highp float; precision highp sampler3D;
|
|
92
|
+
uniform sampler2D frame; uniform float exposure; uniform float exponent;
|
|
93
|
+
uniform bool straightAlpha; in vec2 vUv; out vec4 outputColor;
|
|
94
|
+
${processor.shader}
|
|
95
|
+
void main() {
|
|
96
|
+
vec4 pixel=texture(frame,vUv);
|
|
97
|
+
vec3 linear=pixel.rgb*(pixel.a==0.0 ? exposure : exposure/pixel.a);
|
|
98
|
+
vec3 display=clamp(pow(max(blenderDisplay(vec4(linear,1.0)).rgb,vec3(0.0)),vec3(exponent)),0.0,1.0);
|
|
99
|
+
outputColor=vec4(straightAlpha ? display : display*pixel.a,pixel.a);
|
|
100
|
+
}`,
|
|
101
|
+
});
|
|
102
|
+
this.quad = new FullScreenQuad(this.material);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
render(renderer: THREE.WebGLRenderer, input: Pick<THREE.WebGLRenderTarget, 'texture'>,
|
|
106
|
+
output: THREE.WebGLRenderTarget | null, straightAlpha: boolean): void {
|
|
107
|
+
this.material.uniforms['frame']!.value = input.texture;
|
|
108
|
+
this.material.uniforms['straightAlpha']!.value = straightAlpha;
|
|
109
|
+
renderer.setRenderTarget(output);
|
|
110
|
+
this.quad.render(renderer);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Explicit render/compositor readback, never used by the live frame loop. */
|
|
114
|
+
encodeFrame(renderer: THREE.WebGLRenderer, pixels: Uint16Array, width: number, height: number): Uint8Array {
|
|
115
|
+
const previous = renderer.getRenderTarget();
|
|
116
|
+
const input = new THREE.DataTexture(pixels, width, height, THREE.RGBAFormat, THREE.HalfFloatType);
|
|
117
|
+
input.needsUpdate = true;
|
|
118
|
+
const output = new THREE.WebGLRenderTarget(width, height);
|
|
119
|
+
try {
|
|
120
|
+
this.render(renderer, {texture: input}, output, true);
|
|
121
|
+
const bytes = new Uint8Array(width * height * 4);
|
|
122
|
+
renderer.readRenderTargetPixels(output, 0, 0, width, height, bytes);
|
|
123
|
+
return bytes;
|
|
124
|
+
} finally {
|
|
125
|
+
renderer.setRenderTarget(previous); input.dispose(); output.dispose();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
dispose(): void {
|
|
130
|
+
for (const texture of this.textures) texture.dispose();
|
|
131
|
+
this.material.dispose(); this.quad.dispose();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function createBlenderDisplayTransform(settings: BlenderDisplaySettings): Promise<BlenderDisplayTransform> {
|
|
136
|
+
if (!processors[`${settings.transform}/${settings.look}`])
|
|
137
|
+
throw new Error(`Unimplemented Blender display transform: ${settings.transform}, ${settings.look}`);
|
|
138
|
+
return new BlenderDisplayTransform(settings, await loadDisplayData());
|
|
139
|
+
}
|
|
@@ -24,6 +24,9 @@ interface Binding {
|
|
|
24
24
|
ramps: THREE.DataTexture[];
|
|
25
25
|
/** Named UV layer to the channel the current draw's geometry holds it in. */
|
|
26
26
|
channels: Record<string, number>;
|
|
27
|
+
borrowedFrom?: Binding;
|
|
28
|
+
borrowedSource?: THREE.MeshPhysicalMaterial;
|
|
29
|
+
objectInfoUniforms?: {color: THREE.IUniform<THREE.Vector4>; indexRandom: THREE.IUniform<THREE.Vector2>};
|
|
27
30
|
palette?: {indices: Int32Array; sources: readonly THREE.MeshPhysicalMaterial[]};
|
|
28
31
|
}
|
|
29
32
|
const bindings = new WeakMap<THREE.MeshPhysicalMaterial, Binding>();
|
|
@@ -43,7 +46,7 @@ function compatiblePaletteGraphs(sources: readonly THREE.MeshPhysicalMaterial[])
|
|
|
43
46
|
if (sources.length !== 2 || sources.some(source => pendings.has(source))) return null;
|
|
44
47
|
const graphs = sources.map(source => bindings.get(source)?.compiled);
|
|
45
48
|
const graph = graphs[0];
|
|
46
|
-
if (!graph || failed.has(graph.key) || graphs.some(other => !other || other.key !== graph.key ||
|
|
49
|
+
if (!graph || graph.objectInfo || failed.has(graph.key) || graphs.some(other => !other || other.key !== graph.key ||
|
|
47
50
|
other.declarations !== graph.declarations || other.library !== graph.library ||
|
|
48
51
|
JSON.stringify([other.uvs, other.attributes, other.outputs]) !== JSON.stringify([graph.uvs, graph.attributes, graph.outputs]))) return null;
|
|
49
52
|
// Leave room for the physical material's environment and lighting samplers.
|
|
@@ -160,22 +163,42 @@ export function setMaterialGraph(
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
/** A disposable draw material borrows the source's live uniform values, never
|
|
163
|
-
* its owned ramp textures or geometry-dependent UV selector.
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
* its owned ramp textures or geometry-dependent UV selector. Ordinary copies
|
|
167
|
+
* wait for pending graphs; retained copies draw the previous graph and start
|
|
168
|
+
* their source's compilation from their draw hook. */
|
|
169
|
+
export function borrowGraphDrawBinding(source: THREE.MeshPhysicalMaterial, target: THREE.MeshPhysicalMaterial,
|
|
170
|
+
retainCurrent = false): boolean {
|
|
171
|
+
if (pendings.has(source) && !retainCurrent) return false;
|
|
167
172
|
const sourceBinding = bindings.get(source);
|
|
168
173
|
const binding = sourceBinding && !failed.has(sourceBinding.compiled.key) ? sourceBinding : undefined;
|
|
169
174
|
const held = bindings.get(target);
|
|
170
175
|
if (!binding) {
|
|
171
176
|
if (held) { bindings.delete(target); target.needsUpdate = true; }
|
|
172
|
-
} else if (!held || held.compiled.key !== binding.compiled.key || held.
|
|
173
|
-
bindings.set(target, {compiled: binding.compiled, uniforms: binding.uniforms, ramps: [], channels: {}
|
|
177
|
+
} else if (!held || held.compiled.key !== binding.compiled.key || held.borrowedFrom !== binding) {
|
|
178
|
+
bindings.set(target, {compiled: binding.compiled, uniforms: binding.uniforms, ramps: [], channels: {},
|
|
179
|
+
borrowedFrom: binding, ...(retainCurrent ? {borrowedSource: source} : {})});
|
|
174
180
|
target.needsUpdate = true;
|
|
175
181
|
}
|
|
176
182
|
return true;
|
|
177
183
|
}
|
|
178
184
|
|
|
185
|
+
/** Draw copies borrow authored graph values/textures, but Object Info values
|
|
186
|
+
* belong to ONE object. A distinct material identity makes Three upload them
|
|
187
|
+
* when it switches between objects sharing an authored material. */
|
|
188
|
+
export function bindGraphObjectInfo(material: THREE.MeshPhysicalMaterial,
|
|
189
|
+
info: {color: readonly number[]; index: number; random: number}): void {
|
|
190
|
+
const binding=bindings.get(material);
|
|
191
|
+
if (!binding?.compiled.objectInfo) return;
|
|
192
|
+
const values=binding.objectInfoUniforms ??= {
|
|
193
|
+
color:{value:new THREE.Vector4()}, indexRandom:{value:new THREE.Vector2()},
|
|
194
|
+
};
|
|
195
|
+
if (binding.uniforms['blenderObjectColor']!==values.color) {
|
|
196
|
+
binding.uniforms={...binding.uniforms,blenderObjectColor:values.color,blenderObjectIndexRandom:values.indexRandom};
|
|
197
|
+
}
|
|
198
|
+
values.color.value.fromArray(info.color);
|
|
199
|
+
values.indexRandom.value.set(info.index,info.random);
|
|
200
|
+
}
|
|
201
|
+
|
|
179
202
|
/** A binding's values: uniforms, ramp tables and textures. */
|
|
180
203
|
function fill(binding: Binding, compiled: CompiledGraph, texture: (image: CompiledGraph['images'][number]) => ImageTexture): void {
|
|
181
204
|
binding.compiled = compiled;
|
|
@@ -287,6 +310,10 @@ function channelsMatch(compiled: CompiledGraph, channels: Record<string, number>
|
|
|
287
310
|
export function bindGraphDraw(material: THREE.MeshPhysicalMaterial, geometry: THREE.BufferGeometry,
|
|
288
311
|
renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, object: THREE.Object3D,
|
|
289
312
|
shadowOf: (material: THREE.MeshPhysicalMaterial) => THREE.MeshPhysicalMaterial): void {
|
|
313
|
+
// A per-object copy can be the only visible wearer. Keep its authored
|
|
314
|
+
// material's asynchronous compilation alive while drawing the old graph.
|
|
315
|
+
const source=bindings.get(material)?.borrowedSource;
|
|
316
|
+
if(source && source!==material) bindGraphDraw(source,geometry,renderer,scene,camera,object,shadowOf);
|
|
290
317
|
const retired = retiring.get(material);
|
|
291
318
|
if (retired && ++retired.draws > 1) {
|
|
292
319
|
retired.shadow.dispose();
|
|
@@ -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',
|
|
52
|
+
'ShaderNodeRGBCurve', 'ShaderNodeVectorCurve', 'ShaderNodeFloatCurve', 'ShaderNodeObjectInfo',
|
|
53
53
|
] as const;
|
|
54
54
|
|
|
55
55
|
const nodeSchema = z.object({
|
|
@@ -143,6 +143,8 @@ export interface CompiledGraph {
|
|
|
143
143
|
/** The mesh attributes the graph reads, by Blender name; '' is the mesh's
|
|
144
144
|
* default colour attribute (a Color Attribute node with no layer). */
|
|
145
145
|
readonly attributes: readonly string[];
|
|
146
|
+
/** Requires a draw identity with this object's native inputs. */
|
|
147
|
+
readonly objectInfo: boolean;
|
|
146
148
|
/** Which surface inputs the graph drives, each a GLSL global of `type`. */
|
|
147
149
|
readonly outputs: Readonly<Record<string, {readonly global: string; readonly type: GpuType}>>;
|
|
148
150
|
readonly surface: MaterialGraph['surface'];
|
|
@@ -487,6 +489,15 @@ class Compiler {
|
|
|
487
489
|
body.push(`${outputs[0]!.name} = ${this.uniform(type, prop<number | number[]>(node, 'value'))};`);
|
|
488
490
|
break;
|
|
489
491
|
}
|
|
492
|
+
case 'ShaderNodeObjectInfo': {
|
|
493
|
+
// node_shader_gpu_object_info / node_object_info. Location is the
|
|
494
|
+
// object's origin, never the current fragment's world position.
|
|
495
|
+
const values = ['object_matrices_get().model[3].xyz', 'blenderObjectColor',
|
|
496
|
+
'blenderObjectColor.a', 'blenderObjectIndexRandom.x',
|
|
497
|
+
this.uniform('float', prop<number>(node, 'material_index')), 'blenderObjectIndexRandom.y'];
|
|
498
|
+
values.forEach((v,i)=>{if(outputs[i]) body.push(`${outputs[i]!.name} = ${v};`);});
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
490
501
|
case 'ShaderNodeTexCoord': {
|
|
491
502
|
// node_tex_coord's outputs, in socket order.
|
|
492
503
|
this.s.uvs.add('');
|
|
@@ -946,6 +957,8 @@ class Compiler {
|
|
|
946
957
|
[...this.s.functions, graphBody].join('\n')),
|
|
947
958
|
].join('\n');
|
|
948
959
|
const program = [
|
|
960
|
+
...(Object.values(this.graph.nodes).some(n=>n.type==='ShaderNodeObjectInfo')
|
|
961
|
+
? ['uniform vec4 blenderObjectColor;', 'uniform vec2 blenderObjectIndexRandom;'] : []),
|
|
949
962
|
...[...this.s.uniformTypes].map(([n, t]) => `uniform ${t} ${n};`),
|
|
950
963
|
...this.images.map(i => i.tiled
|
|
951
964
|
? `uniform highp sampler2DArray ${i.uniform};\nuniform highp sampler2D ${i.uniform}Map;`
|
|
@@ -966,6 +979,7 @@ class Compiler {
|
|
|
966
979
|
ramps: this.ramps,
|
|
967
980
|
uvs: [...this.s.uvs],
|
|
968
981
|
attributes: [...this.s.attributes],
|
|
982
|
+
objectInfo: Object.values(this.graph.nodes).some(n=>n.type==='ShaderNodeObjectInfo'),
|
|
969
983
|
outputs,
|
|
970
984
|
surface: this.graph.surface,
|
|
971
985
|
closure: closure !== undefined,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import {bindGraphObjectInfo, materialGraph} from './blender-graph-material';
|
|
3
|
+
import {syncPhysicalDrawMaterial} from './blender-physical-material';
|
|
4
|
+
|
|
5
|
+
export interface BlenderObjectInfo {
|
|
6
|
+
readonly color: readonly number[];
|
|
7
|
+
readonly index: number;
|
|
8
|
+
readonly random: number;
|
|
9
|
+
}
|
|
10
|
+
type Draw = {source: THREE.MeshPhysicalMaterial; mesh: THREE.Mesh; material: THREE.MeshPhysicalMaterial; version:number};
|
|
11
|
+
|
|
12
|
+
/** Per-object shader inputs cannot be shared between material identities:
|
|
13
|
+
* Three skips uploads when successive objects wear the same material. Keep
|
|
14
|
+
* small draw copies, sharing every image/ramp, rather than copying geometry or
|
|
15
|
+
* flattening Random to a constant. Instance/palette optimization stays out of
|
|
16
|
+
* these draws until it can carry every native per-instance input. */
|
|
17
|
+
export class BlenderObjectInfoMaterials {
|
|
18
|
+
private readonly draws=new Map<string,Draw>();
|
|
19
|
+
private readonly sources=new Map<THREE.MeshPhysicalMaterial,string|null>();
|
|
20
|
+
private used=new Set<string>();
|
|
21
|
+
|
|
22
|
+
begin(): void {this.used.clear();this.sources.clear();}
|
|
23
|
+
|
|
24
|
+
get(source: THREE.Material, mesh: THREE.Mesh): THREE.Material {
|
|
25
|
+
if (!(source instanceof THREE.MeshPhysicalMaterial)) return source;
|
|
26
|
+
const graph=materialGraph(source);
|
|
27
|
+
this.sources.set(source,graph?.key??null);
|
|
28
|
+
if (!graph?.objectInfo) return source;
|
|
29
|
+
const info=mesh.userData['blenderObjectInfo'] as BlenderObjectInfo|undefined;
|
|
30
|
+
if (!info) throw new Error(`Object Info inputs missing for Blender object ${mesh.name}`);
|
|
31
|
+
const key=`${mesh.uuid}:${source.uuid}`;
|
|
32
|
+
this.used.add(key);
|
|
33
|
+
let draw=this.draws.get(key);
|
|
34
|
+
if (!draw) {
|
|
35
|
+
const material=syncPhysicalDrawMaterial(source,undefined,true);
|
|
36
|
+
if (!material) return source;
|
|
37
|
+
draw={source,mesh,material,version:source.version};
|
|
38
|
+
this.draws.set(key,draw);
|
|
39
|
+
}
|
|
40
|
+
this.sync(draw,info);
|
|
41
|
+
return draw.material;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
end(): void {
|
|
45
|
+
for(const [key,draw] of this.draws) if(!this.used.has(key)) {
|
|
46
|
+
draw.material.dispose();this.draws.delete(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A pending authored graph can finish without another native export. */
|
|
51
|
+
needsRemap(): boolean {
|
|
52
|
+
return [...this.sources].some(([source,key])=>(materialGraph(source)?.key??null)!==key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
refresh(): void {
|
|
56
|
+
for(const draw of this.draws.values()) {
|
|
57
|
+
const info=draw.mesh.userData['blenderObjectInfo'] as BlenderObjectInfo|undefined;
|
|
58
|
+
if (!info) throw new Error(`Object Info inputs missing for Blender object ${draw.mesh.name}`);
|
|
59
|
+
this.sync(draw,info);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private sync(draw:Draw,info:BlenderObjectInfo): void {
|
|
64
|
+
if(!syncPhysicalDrawMaterial(draw.source,draw.material,true))
|
|
65
|
+
throw new Error(`Object Info material ${draw.source.name} lost its draw binding`);
|
|
66
|
+
if(draw.version!==draw.source.version) draw.material.needsUpdate=true;
|
|
67
|
+
draw.version=draw.source.version;
|
|
68
|
+
bindGraphObjectInfo(draw.material,info);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clear(): void {
|
|
72
|
+
for(const draw of this.draws.values()) draw.material.dispose();
|
|
73
|
+
this.draws.clear();this.sources.clear();this.used.clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -140,10 +140,11 @@ export function physicalPaletteMaterial(compiled: CompiledGraph, sources: readon
|
|
|
140
140
|
|
|
141
141
|
/** Copy physical values into an independently cached draw form. Textures and
|
|
142
142
|
* graph uniform values remain borrowed from the authoritative source material. */
|
|
143
|
-
export function syncPhysicalDrawMaterial(source: THREE.Material, target?: THREE.MeshPhysicalMaterial
|
|
143
|
+
export function syncPhysicalDrawMaterial(source: THREE.Material, target?: THREE.MeshPhysicalMaterial,
|
|
144
|
+
retainCurrentGraph = false): THREE.MeshPhysicalMaterial | null {
|
|
144
145
|
if (!(source instanceof THREE.MeshPhysicalMaterial) || !uniforms.has(source) || !materialDrawHooksSupported(source)) return null;
|
|
145
146
|
const copy = target ?? graphShadow(source);
|
|
146
|
-
if (!borrowGraphDrawBinding(source, copy)) {
|
|
147
|
+
if (!borrowGraphDrawBinding(source, copy, retainCurrentGraph)) {
|
|
147
148
|
if (!target) copy.dispose();
|
|
148
149
|
return null;
|
|
149
150
|
}
|
|
@@ -159,6 +160,16 @@ export function syncPhysicalDrawMaterial(source: THREE.Material, target?: THREE.
|
|
|
159
160
|
return copy;
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
/** A copy of a presented material that ONE object can wear and a script can recolour (Model
|
|
164
|
+
* Play's `tint`/`setOpacity`): the same hooks, physical values, maps and private uniforms,
|
|
165
|
+
* drawn from the material's constants -- `color`, `opacity`, `map`, `emissive` -- with no
|
|
166
|
+
* node graph, since a graph's Base Color and Alpha replace those constants in the shader.
|
|
167
|
+
* Null for a material that is not one of the presenter's. The caller owns and disposes it. */
|
|
168
|
+
export function ownedSurfaceMaterial(source: THREE.Material): THREE.MeshPhysicalMaterial | null {
|
|
169
|
+
if (!(source instanceof THREE.MeshPhysicalMaterial) || !uniforms.has(source) || !materialDrawHooksSupported(source)) return null;
|
|
170
|
+
return graphShadow(source);
|
|
171
|
+
}
|
|
172
|
+
|
|
162
173
|
export function applyPhysicalMaterial(material: THREE.MeshPhysicalMaterial, input?: Physical,
|
|
163
174
|
clips: {map?: boolean; roughness?: boolean; normal?: boolean} = {}): void {
|
|
164
175
|
const data = input ?? defaults;
|
|
@@ -240,7 +240,7 @@ const BONE_SOLID_VERTEX = /* glsl */ `
|
|
|
240
240
|
* ONLY THE `_fragment` HALF: three PREPENDS `colorspace_pars_fragment` to every
|
|
241
241
|
* `ShaderMaterial` already, so including it here declared `LinearTransferOETF`,
|
|
242
242
|
* `sRGBTransferEOTF` and `sRGBTransferOETF` twice and the whole program refused
|
|
243
|
-
* to compile ("function already has a body", measured live in `
|
|
243
|
+
* to compile ("function already has a body", measured live in `cyclotron console`).
|
|
244
244
|
*/
|
|
245
245
|
const BONE_SOLID_FRAGMENT = /* glsl */ `
|
|
246
246
|
varying vec3 vColor;
|
|
@@ -264,7 +264,7 @@ export const SOLID_LIGHTS: readonly {
|
|
|
264
264
|
* and AgX within 3, so the curve is NOT what row 1's 13-vs-30 spread was made
|
|
265
265
|
* of and the stage keeps its own operator).
|
|
266
266
|
*
|
|
267
|
-
* THE ROUNDS, on a `
|
|
267
|
+
* THE ROUNDS, on a `cyclotron create` scaffold driven through `cyclotron
|
|
268
268
|
* screenshot editor`, 30x30 means (std 0 — every patch inside one flat face)
|
|
269
269
|
* of the factory cube's three visible faces against Blender 5.2's own frame,
|
|
270
270
|
* (141,143,145) top / (129,131,131) left / (111,112,113) front:
|
|
@@ -435,6 +435,7 @@ export type WorldExpression =
|
|
|
435
435
|
}
|
|
436
436
|
| {
|
|
437
437
|
kind: 'sky';
|
|
438
|
+
sky_model?: 'SINGLE_SCATTERING' | 'MULTIPLE_SCATTERING' | undefined;
|
|
438
439
|
sun_elevation: number;
|
|
439
440
|
sun_rotation: number;
|
|
440
441
|
altitude: number;
|
|
@@ -531,6 +532,7 @@ const worldExpression: z.ZodType<WorldExpression> = z.lazy(() =>
|
|
|
531
532
|
z
|
|
532
533
|
.object({
|
|
533
534
|
kind: z.literal('sky'),
|
|
535
|
+
sky_model: z.enum(['SINGLE_SCATTERING', 'MULTIPLE_SCATTERING']).optional(),
|
|
534
536
|
sun_elevation: scalar,
|
|
535
537
|
sun_rotation: scalar,
|
|
536
538
|
altitude: scalar,
|
|
@@ -28,6 +28,7 @@ import { fitModelDirectionalShadow, visibleShadowReceivers } from './blender-run
|
|
|
28
28
|
import { volumeMesh, volumeSchema } from './blender-runtime-volume';
|
|
29
29
|
import { applyPhysicalMaterial, applyWorldExtinction, physicalMaterialSchema } from './blender-physical-material';
|
|
30
30
|
import { prepareGraphGeometry, setMaterialGraph } from './blender-graph-material';
|
|
31
|
+
import {BlenderObjectInfoMaterials} from './blender-object-info-materials';
|
|
31
32
|
import { type CompiledGraph, compileMaterialGraph, graphSeeThrough, materialGraphSchema } from './blender-node-graph';
|
|
32
33
|
import {worldMedium, WorldVolumePass} from './blender-world-volume';
|
|
33
34
|
import { BlenderTextureSamplers } from './blender-texture-samplers';
|
|
@@ -525,6 +526,9 @@ export const frameSchema = z
|
|
|
525
526
|
* the evaluated bounds (`blender-graph-material.ts`'s orco). */
|
|
526
527
|
texspace: z.tuple([z.tuple([scalar, scalar, scalar]), z.tuple([scalar, scalar, scalar])]).optional(),
|
|
527
528
|
default_color: z.string().optional(),
|
|
529
|
+
object_info: z.object({
|
|
530
|
+
color:z.tuple([scalar,scalar,scalar,scalar]),index:scalar,random:scalar,
|
|
531
|
+
}).strict().optional(),
|
|
528
532
|
})
|
|
529
533
|
.strict(),
|
|
530
534
|
),
|
|
@@ -661,6 +665,7 @@ export class BlenderRuntimeView {
|
|
|
661
665
|
{ signature: string; mesh: ReturnType<typeof volumeMesh> }
|
|
662
666
|
>();
|
|
663
667
|
private readonly materials = new Map<string, THREE.MeshPhysicalMaterial>();
|
|
668
|
+
private readonly objectInfoMaterials=new BlenderObjectInfoMaterials();
|
|
664
669
|
private readonly lights = new Map<string, THREE.Light>();
|
|
665
670
|
private readonly lighting = new ViewportLighting();
|
|
666
671
|
private readonly world = new WorldBackground();
|
|
@@ -1253,6 +1258,7 @@ export class BlenderRuntimeView {
|
|
|
1253
1258
|
private applyWorkbench(): void {
|
|
1254
1259
|
const solid = this.workbench && !this.rendered;
|
|
1255
1260
|
const used = new Set<string>();
|
|
1261
|
+
this.objectInfoMaterials.begin();
|
|
1256
1262
|
for (const obj of this.frame?.objects ?? []) {
|
|
1257
1263
|
if (obj.mesh === null || obj.volume) continue;
|
|
1258
1264
|
const mesh = this.objects.get(obj.id) as THREE.Mesh | undefined;
|
|
@@ -1260,10 +1266,12 @@ export class BlenderRuntimeView {
|
|
|
1260
1266
|
const slots: readonly (string | null)[] = obj.materials.length ? obj.materials : [null];
|
|
1261
1267
|
const shown = slots.map((id) => {
|
|
1262
1268
|
const authored = id === null ? this.fallback : (this.materials.get(id) ?? this.fallback);
|
|
1263
|
-
return solid ? this.workbenchFor(id, authored.side, used) : authored;
|
|
1269
|
+
return solid ? this.workbenchFor(id, authored.side, used) : this.objectInfoMaterials.get(authored,mesh);
|
|
1264
1270
|
});
|
|
1265
1271
|
mesh.material = obj.materials.length ? shown : shown[0]!;
|
|
1272
|
+
prepareGraphGeometry(mesh);
|
|
1266
1273
|
}
|
|
1274
|
+
this.objectInfoMaterials.end();
|
|
1267
1275
|
// Released as soon as no surface wears it: a dragged viewport colour makes one per value.
|
|
1268
1276
|
for (const [key, material] of this.workbenchMaterials) {
|
|
1269
1277
|
if (used.has(key)) continue;
|
|
@@ -1300,6 +1308,10 @@ export class BlenderRuntimeView {
|
|
|
1300
1308
|
* actual camera. Transparent instance runs are camera-order dependent. */
|
|
1301
1309
|
prepareDraw(camera: THREE.Camera, options?: {interactive: boolean; height: number; multiDraw?: boolean; renderer?: THREE.WebGLRenderer}): () => void {
|
|
1302
1310
|
try {
|
|
1311
|
+
if (!this.workbench || this.rendered) {
|
|
1312
|
+
if(this.objectInfoMaterials.needsRemap()) this.applyWorkbench();
|
|
1313
|
+
this.objectInfoMaterials.refresh();
|
|
1314
|
+
}
|
|
1303
1315
|
const start = performance.now();
|
|
1304
1316
|
this.instances.prepareDraw(camera);
|
|
1305
1317
|
this.root.updateMatrixWorld(true);
|
|
@@ -2052,6 +2064,8 @@ export class BlenderRuntimeView {
|
|
|
2052
2064
|
else delete object.userData['blenderTexspace'];
|
|
2053
2065
|
if (obj.default_color !== undefined) object.userData['blenderDefaultColor'] = obj.default_color;
|
|
2054
2066
|
else delete object.userData['blenderDefaultColor'];
|
|
2067
|
+
if(obj.object_info) object.userData['blenderObjectInfo']=obj.object_info;
|
|
2068
|
+
else delete object.userData['blenderObjectInfo'];
|
|
2055
2069
|
if (obj.mesh !== null) {
|
|
2056
2070
|
const mesh = object as THREE.Mesh;
|
|
2057
2071
|
mesh.geometry = this.meshes.get(obj.mesh)!.geometry;
|
|
@@ -2397,6 +2411,7 @@ export class BlenderRuntimeView {
|
|
|
2397
2411
|
}
|
|
2398
2412
|
|
|
2399
2413
|
private clear() {
|
|
2414
|
+
this.objectInfoMaterials.clear();
|
|
2400
2415
|
this.materialRanges.clear();
|
|
2401
2416
|
this.motionGeometry.clear();
|
|
2402
2417
|
this.transparentInstances.clear();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/* SPDX-FileCopyrightText: 2011-2020 Blender Authors
|
|
2
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
*
|
|
4
|
+
* Single-scattering precomputation from intern/sky/source/sky_single_scattering.cpp,
|
|
5
|
+
* Blender revision 071e080 (the public source retained on this machine).
|
|
6
|
+
* The atmosphere, quadrature, spectrum and nonlinear LUT layout are Blender's.
|
|
7
|
+
* Float operations retain the native model's precision at Earth-scale positions.
|
|
8
|
+
*/
|
|
9
|
+
import type {SkyParameters} from './blender-sky';
|
|
10
|
+
|
|
11
|
+
const f = Math.fround;
|
|
12
|
+
const RAYLEIGH_SCALE = 8000, MIE_SCALE = 1200, MIE_COEFF = f(2e-5);
|
|
13
|
+
const MIE_G = f(0.76), SQR_G = f(MIE_G * MIE_G);
|
|
14
|
+
const EARTH_RADIUS = 6360000, ATMOSPHERE_RADIUS = 6420000;
|
|
15
|
+
const PI = f(Math.PI), HALF_PI = f(Math.PI / 2), TWO_PI = f(Math.PI * 2);
|
|
16
|
+
const IRRADIANCE = [1.45756829855593, 1.5659630555973838, 1.6514844906767046, 1.7149624273720931, 1.7579798380502054, 1.7825640788592454, 1.7909510847583856, 1.7854155013341066, 1.7681555486430685, 1.741220696472504, 1.7064712716494368, 1.6655608745273989, 1.6199343724245185, 1.5708359736889208, 1.5193233505930548, 1.466284949652144, 1.4124585274017245, 1.358449619703841, 1.3047491384473928, 1.2517496327261082, 1.1997599875542062].map(f);
|
|
17
|
+
const RAYLEIGH_COEFF = [5.424820087636473e-05, 4.418549866505454e-05, 3.635151910165377e-05, 3.017929012024763e-05, 2.526320226989157e-05, 2.130859310621843e-05, 1.809838025320633e-05, 1.547057129129042e-05, 1.33028497733685e-05, 1.150184784075764e-05, 9.99557429990163e-06, 8.72799973630707e-06, 7.65513700977967e-06, 6.74217203751443e-06, 5.96134125832052e-06, 5.2903459806581e-06, 4.71115687557433e-06, 4.20910481110487e-06, 3.77218381260133e-06, 3.3905125547728e-06, 3.05591531679811e-06].map(f);
|
|
18
|
+
const OZONE_COEFF = [3.25126849861e-09, 5.85395365047e-09, 1.977191155085e-08, 7.309568762914e-08, 2.0084561514287e-07, 4.0383958096161e-07, 6.3551335912363e-07, 9.670704118097e-07, 1.5479740042441e-06, 2.09038647223331e-06, 2.46128056164565e-06, 2.73551299461512e-06, 2.15125863128643e-06, 1.59051840791988e-06, 1.12356197979857e-06, 7.3527551487574e-07, 4.6450130357806e-07, 3.3096079921048e-07, 2.2512612292678e-07, 1.487912926649e-07, 1.6828623364192e-07].map(f);
|
|
19
|
+
const CMF_XYZ = [[0.001368, 3.9e-05, 0.006450001], [0.01431, 0.000396, 0.06785001], [0.13438, 0.004, 0.6456], [0.34828, 0.023, 1.74706], [0.2908, 0.06, 1.6692], [0.09564, 0.13902, 0.8129501], [0.0049, 0.323, 0.272], [0.06327, 0.71, 0.07824999], [0.2904, 0.954, 0.0203], [0.5945, 0.995, 0.0039], [0.9163, 0.87, 0.001650001], [1.0622, 0.631, 0.0008], [0.8544499, 0.381, 0.00019], [0.4479, 0.175, 2e-05], [0.1649, 0.061, 0.0], [0.04677, 0.017, 0.0], [0.01135916, 0.004102, 0.0], [0.002899327, 0.001047, 0.0], [0.0006900786, 0.0002492, 0.0], [0.0001661505, 6e-05, 0.0], [4.150994e-05, 1.499e-05, 0.0]].map(row => row.map(f));
|
|
20
|
+
const QUADRATURE_NODES = [0.006811185292, 0.03614807107, 0.09004346519, 0.1706680068, 0.2818362161, 0.4303406404, 0.6296271457, 0.9145252695].map(f);
|
|
21
|
+
const QUADRATURE_WEIGHTS = [0.01750893642, 0.04135477391, 0.06678839063, 0.09507698807, 0.1283416365, 0.1707430204, 0.2327233347, 0.3562490486].map(f);
|
|
22
|
+
|
|
23
|
+
type V = readonly [number, number, number];
|
|
24
|
+
const add = (a: V, b: V): V => [f(a[0] + b[0]), f(a[1] + b[1]), f(a[2] + b[2])];
|
|
25
|
+
const scale = (a: V, b: number): V => [f(a[0] * b), f(a[1] * b), f(a[2] * b)];
|
|
26
|
+
const multiply = (a: V, b: V): V => [f(a[0] * b[0]), f(a[1] * b[1]), f(a[2] * b[2])];
|
|
27
|
+
const dot = (a: V, b: V): number => f(f(f(a[0] * b[0]) + f(a[1] * b[1])) + f(a[2] * b[2]));
|
|
28
|
+
const length = (a: V): number => f(Math.sqrt(dot(a, a)));
|
|
29
|
+
const direction = (latitude: number, longitude: number): V => [
|
|
30
|
+
f(f(Math.cos(latitude)) * f(Math.cos(longitude))),
|
|
31
|
+
f(f(Math.cos(latitude)) * f(Math.sin(longitude))), f(Math.sin(latitude)),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function atmosphereDistance(position: V, ray: V): number {
|
|
35
|
+
const b = f(2 * dot(ray, position));
|
|
36
|
+
const c = f(dot(position, position) - f(ATMOSPHERE_RADIUS * ATMOSPHERE_RADIUS));
|
|
37
|
+
return f(f(-b + f(Math.sqrt(f(f(b * b) - f(4 * c))))) / 2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function earthBlocks(position: V, ray: V): boolean {
|
|
41
|
+
if (ray[2] >= 0) return false;
|
|
42
|
+
const b = f(2 * dot(ray, position));
|
|
43
|
+
const c = f(dot(position, position) - f(EARTH_RADIUS * EARTH_RADIUS));
|
|
44
|
+
return f(f(b * b) - f(4 * c)) >= 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function density(position: V): V {
|
|
48
|
+
const height = f(length(position) - EARTH_RADIUS);
|
|
49
|
+
return [f(Math.exp(f(-height / RAYLEIGH_SCALE))), f(Math.exp(f(-height / MIE_SCALE))),
|
|
50
|
+
f(Math.max(0, f(1 - f(Math.abs(f(height - 25000)) / 15000))))];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Native quadrature integrates the density factors rather than 21 spectra. */
|
|
54
|
+
function opticalDepth(position: V, ray: V): V {
|
|
55
|
+
const end = add(position, scale(ray, atmosphereDistance(position, ray)));
|
|
56
|
+
const distance = length([f(end[0] - position[0]), f(end[1] - position[1]), f(end[2] - position[2])]);
|
|
57
|
+
const segment = scale(ray, distance);
|
|
58
|
+
let depth: V = [0, 0, 0];
|
|
59
|
+
for (let i = 0; i < QUADRATURE_NODES.length; i++)
|
|
60
|
+
depth = add(depth, scale(density(add(position, scale(segment, QUADRATURE_NODES[i]!))), QUADRATURE_WEIGHTS[i]!));
|
|
61
|
+
return scale(depth, distance);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function spectrumToXyz(spectrum: Float32Array): V {
|
|
65
|
+
let x = 0, y = 0, z = 0;
|
|
66
|
+
for (let i = 0; i < spectrum.length; i++) {
|
|
67
|
+
x = f(x + f(CMF_XYZ[i]![0]! * spectrum[i]!));
|
|
68
|
+
y = f(y + f(CMF_XYZ[i]![1]! * spectrum[i]!));
|
|
69
|
+
z = f(z + f(CMF_XYZ[i]![2]! * spectrum[i]!));
|
|
70
|
+
}
|
|
71
|
+
return [f(x * 20), f(y * 20), f(z * 20)];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function inscattering(ray: V, sun: V, origin: V, factors: V): V {
|
|
75
|
+
const end = add(origin, scale(ray, atmosphereDistance(origin, ray)));
|
|
76
|
+
const rayLength = length([f(end[0] - origin[0]), f(end[1] - origin[1]), f(end[2] - origin[2])]);
|
|
77
|
+
const step = f(rayLength / 32), segment = scale(ray, step);
|
|
78
|
+
const mu = dot(ray, sun), mu2 = f(mu * mu);
|
|
79
|
+
const rayleigh = f(f(f(.1875 * f(1 / Math.PI)) * f(1 + mu2)));
|
|
80
|
+
const denominator = f(f(f(8 * PI) * f(2 + SQR_G)) * f(Math.pow(f(f(1 + SQR_G) - f(f(2 * MIE_G) * mu)), 1.5)));
|
|
81
|
+
const mie = f(f(f(f(3 * f(1 - SQR_G)) * f(1 + mu2))) / denominator);
|
|
82
|
+
const result = new Float32Array(21);
|
|
83
|
+
let depth: V = [0, 0, 0], position = add(origin, scale(segment, .5));
|
|
84
|
+
for (let i = 0; i < 32; i++) {
|
|
85
|
+
const d = multiply(factors, density(position));
|
|
86
|
+
depth = add(depth, scale(d, step));
|
|
87
|
+
if (!earthBlocks(position, sun)) {
|
|
88
|
+
const total = add(depth, multiply(factors, opticalDepth(position, sun)));
|
|
89
|
+
for (let wl = 0; wl < 21; wl++) {
|
|
90
|
+
const extinction = f(f(f(total[0] * RAYLEIGH_COEFF[wl]!) + f(total[1] * f(1.11 * MIE_COEFF))) + f(total[2] * OZONE_COEFF[wl]!));
|
|
91
|
+
const attenuation = f(Math.exp(-extinction));
|
|
92
|
+
const scattering = f(f(rayleigh * f(d[0] * RAYLEIGH_COEFF[wl]!)) + f(mie * f(d[1] * MIE_COEFF)));
|
|
93
|
+
result[wl] = f(result[wl]! + f(f(f(attenuation * scattering) * IRRADIANCE[wl]!) * step));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
position = add(position, segment);
|
|
97
|
+
}
|
|
98
|
+
return spectrumToXyz(result);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** SKY_single_scattering_precompute_texture: upper sky mirrored in longitude,
|
|
102
|
+
* lower sky faded from the horizon, with more latitude samples near it. */
|
|
103
|
+
export function precomputeSingleScatteringSky(p: SkyParameters, width = 512, height = 256): Float32Array {
|
|
104
|
+
const pixels = new Float32Array(width * height * 3);
|
|
105
|
+
const halfWidth = width / 2, halfHeight = height / 2;
|
|
106
|
+
const origin: V = [0, 0, f(EARTH_RADIUS + Math.min(59999, Math.max(1, f(p.altitude))))];
|
|
107
|
+
const sun = direction(f(p.sunElevation), 0);
|
|
108
|
+
const factors: V = [f(p.airDensity), f(p.aerosolDensity), f(p.ozoneDensity)];
|
|
109
|
+
const longitudeStep = f(TWO_PI / width);
|
|
110
|
+
for (let y = halfHeight; y < height; y++) {
|
|
111
|
+
const latitudeStep = f(f(y / halfHeight) - 1);
|
|
112
|
+
const latitude = f(HALF_PI * f(latitudeStep * latitudeStep));
|
|
113
|
+
for (let x = 0; x < halfWidth; x++) {
|
|
114
|
+
const longitude = f(f(longitudeStep * x) - PI);
|
|
115
|
+
const xyz = inscattering(direction(latitude, longitude), sun, origin, factors);
|
|
116
|
+
pixels.set(xyz, (y * width + x) * 3);
|
|
117
|
+
pixels.set(xyz, (y * width + width - x - 1) * 3);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (let y = 0; y < halfHeight; y++) {
|
|
121
|
+
const latitudeStep = f(f(y / halfHeight) - 1);
|
|
122
|
+
const z = f(Math.sin(f(HALF_PI * f(latitudeStep * latitudeStep))));
|
|
123
|
+
let fade = 0;
|
|
124
|
+
if (z < .4) { fade = f(1 - f(z * 2.5)); fade = f(f(fade * fade) * fade); }
|
|
125
|
+
for (let x = 0; x < width * 3; x++) pixels[y * width * 3 + x] = f(pixels[halfHeight * width * 3 + x]! * fade);
|
|
126
|
+
}
|
|
127
|
+
return pixels;
|
|
128
|
+
}
|