@volter/blender-engine 0.1.127 → 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.
@@ -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. Pending graphs
164
- * must first draw through their source so its asynchronous compile can finish. */
165
- export function borrowGraphDrawBinding(source: THREE.MeshPhysicalMaterial, target: THREE.MeshPhysicalMaterial): boolean {
166
- if (pendings.has(source)) return false;
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.uniforms !== binding.uniforms) {
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): THREE.MeshPhysicalMaterial | null {
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
  }
@@ -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 `volter-model-editor console`).
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 `model-editor create` scaffold driven through `volter-model-editor
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
+ }