@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
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `Linear CIE-XYZ D65`.
|
|
16
16
|
*/
|
|
17
17
|
import * as THREE from 'three';
|
|
18
|
-
import {precomputeSingleScatteringSky} from './blender-single-scattering-sky';
|
|
18
|
+
import {precomputeSingleScatteringSky, precomputeSingleScatteringSun} from './blender-single-scattering-sky';
|
|
19
19
|
|
|
20
20
|
const M_PI_2_F = 1.5707963267948966;
|
|
21
21
|
const M_2PI_F = 6.2831853071795864;
|
|
@@ -104,6 +104,9 @@ const aerosolDensity = (h: number): number =>
|
|
|
104
104
|
export interface SkyParameters {
|
|
105
105
|
/** Omitted in older frames, whose only supported model was multiple scattering. */
|
|
106
106
|
model?: 'SINGLE_SCATTERING' | 'MULTIPLE_SCATTERING';
|
|
107
|
+
sunDisc?: boolean | undefined;
|
|
108
|
+
sunSize?: number | undefined;
|
|
109
|
+
sunIntensity?: number | undefined;
|
|
107
110
|
sunElevation: number;
|
|
108
111
|
sunRotation: number;
|
|
109
112
|
altitude: number;
|
|
@@ -399,6 +402,14 @@ export function primeSkyTexture(p: SkyParameters, pixels: Float32Array): void {
|
|
|
399
402
|
}
|
|
400
403
|
}
|
|
401
404
|
|
|
405
|
+
/** Cycles' Z-up solar direction: rotation zero points along +Y. */
|
|
406
|
+
export function skySunDirection(p: Pick<SkyParameters, 'sunElevation' | 'sunRotation'>): THREE.Vector3 {
|
|
407
|
+
// sky_radiance_nishita uses spherical_to_direction(elevation - PI/2,
|
|
408
|
+
// (2PI - rotation) - PI/2), after SkyTextureNode::simplify_settings.
|
|
409
|
+
return new THREE.Vector3(Math.cos(p.sunElevation) * Math.sin(p.sunRotation),
|
|
410
|
+
Math.cos(p.sunElevation) * Math.cos(p.sunRotation), Math.sin(p.sunElevation));
|
|
411
|
+
}
|
|
412
|
+
|
|
402
413
|
/**
|
|
403
414
|
* One Sky Texture node as a field over directions, in Blender's Z-up frame.
|
|
404
415
|
*
|
|
@@ -422,6 +433,12 @@ export function skyField(p: SkyParameters): (direction: THREE.Vector3) => THREE.
|
|
|
422
433
|
primeSkyTexture(p, pixels);
|
|
423
434
|
}
|
|
424
435
|
const colour = new THREE.Vector3();
|
|
436
|
+
const diameter = p.sunSize ?? 0;
|
|
437
|
+
const solar = p.model === 'SINGLE_SCATTERING' && p.sunDisc && diameter > 0
|
|
438
|
+
? precomputeSingleScatteringSun(p, diameter) : null;
|
|
439
|
+
const sun = skySunDirection(p);
|
|
440
|
+
const earthAngle = -Math.acos(6360000 / (6360000 + Math.min(59999, Math.max(1, p.altitude))));
|
|
441
|
+
const cross = new THREE.Vector3();
|
|
425
442
|
return (direction) => {
|
|
426
443
|
const horizontal = Math.hypot(direction.x, direction.y);
|
|
427
444
|
const dirElevation = Math.atan2(direction.z, horizontal);
|
|
@@ -456,6 +473,14 @@ export function skyField(p: SkyParameters): (direction: THREE.Vector3) => THREE.
|
|
|
456
473
|
);
|
|
457
474
|
xyz[i] = mix(top, bottom, ty);
|
|
458
475
|
}
|
|
476
|
+
if (solar && dirElevation > earthAngle) {
|
|
477
|
+
const angle = Math.atan2(cross.crossVectors(direction, sun).length(), direction.dot(sun));
|
|
478
|
+
if (angle < diameter / 2) {
|
|
479
|
+
const limb = 1 - .6 * (1 - Math.sqrt(Math.max(0, 1 - (angle / (diameter / 2)) ** 2)));
|
|
480
|
+
const t = (dirElevation - p.sunElevation) / diameter + .5;
|
|
481
|
+
for (let i = 0; i < 3; i++) xyz[i]! += mix(solar[0][i]!, solar[1][i]!, t) * (p.sunIntensity ?? 1) * limb;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
459
484
|
return colour.set(
|
|
460
485
|
XYZ_TO_RGB[0]![0]! * xyz[0]! + XYZ_TO_RGB[0]![1]! * xyz[1]! + XYZ_TO_RGB[0]![2]! * xyz[2]!,
|
|
461
486
|
XYZ_TO_RGB[1]![0]! * xyz[0]! + XYZ_TO_RGB[1]![1]! * xyz[1]! + XYZ_TO_RGB[1]![2]! * xyz[2]!,
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/** Directional world-light visibility. One shared depth atlas replaces repeated
|
|
2
|
+
* per-pixel triangle traversal. Geometry/placements are derived draw resources;
|
|
3
|
+
* Blender data and the visible canvas are never changed. This is finite
|
|
4
|
+
* directional quadrature, not indirect light transport. */
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
|
|
7
|
+
const SAMPLE_COUNT=16, TILES=4, TILE_SIZE=512;
|
|
8
|
+
type Segment={mesh:THREE.Mesh; geometry:THREE.BufferGeometry; position:THREE.BufferAttribute|THREE.InterleavedBufferAttribute;
|
|
9
|
+
index:THREE.BufferAttribute|null; positionVersion:number; indexVersion:number; start:number; count:number;
|
|
10
|
+
offset:number; matrix:THREE.Matrix4; shown:boolean; bounds:THREE.Box3; dynamic:boolean};
|
|
11
|
+
const version=(attribute:THREE.BufferAttribute|THREE.InterleavedBufferAttribute)=>
|
|
12
|
+
'isInterleavedBufferAttribute' in attribute ? attribute.data.version : attribute.version;
|
|
13
|
+
function shown(mesh:THREE.Mesh):boolean {
|
|
14
|
+
if(!mesh.castShadow || !mesh.layers.isEnabled(0))return false;
|
|
15
|
+
for(let parent:THREE.Object3D|null=mesh;parent;parent=parent.parent)if(!parent.visible)return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class BlenderWorldVisibility {
|
|
20
|
+
readonly uniforms={
|
|
21
|
+
blenderWorldVisibilityEnabled:{value:false},
|
|
22
|
+
blenderWorldDepth:{value:null as THREE.Texture|null},
|
|
23
|
+
blenderWorldDynamicDepth:{value:null as THREE.Texture|null},
|
|
24
|
+
blenderWorldHasDynamicDepth:{value:false},
|
|
25
|
+
blenderWorldRadiance:{value:null as THREE.Texture|null},
|
|
26
|
+
blenderWorldDirections:{value:Array.from({length:SAMPLE_COUNT},(_,i)=>{
|
|
27
|
+
const z=1-2*(i+.5)/SAMPLE_COUNT,angle=i*2.399963229728653;
|
|
28
|
+
return new THREE.Vector3(Math.sqrt(1-z*z)*Math.cos(angle),z,Math.sqrt(1-z*z)*Math.sin(angle));
|
|
29
|
+
})},
|
|
30
|
+
blenderWorldMatrices:{value:Array.from({length:SAMPLE_COUNT},()=>new THREE.Matrix4())},
|
|
31
|
+
blenderWorldToNative:{value:new THREE.Matrix3()},
|
|
32
|
+
blenderWorldRayOffset:{value:1e-5},
|
|
33
|
+
};
|
|
34
|
+
private target:THREE.WebGLRenderTarget|null=null;
|
|
35
|
+
private dynamicTarget:THREE.WebGLRenderTarget|null=null;
|
|
36
|
+
private readonly dynamicScene=new THREE.Scene();
|
|
37
|
+
private readonly dynamicMeshes=new Map<THREE.Mesh,THREE.Mesh>();
|
|
38
|
+
private readonly scene=new THREE.Scene();
|
|
39
|
+
private readonly geometry=new THREE.BufferGeometry();
|
|
40
|
+
private readonly material=new THREE.MeshDepthMaterial({side:THREE.DoubleSide});
|
|
41
|
+
private readonly mesh=new THREE.Mesh(this.geometry,this.material);
|
|
42
|
+
private readonly camera=new THREE.OrthographicCamera();
|
|
43
|
+
private segments:Segment[]=[];
|
|
44
|
+
private dirty=true;
|
|
45
|
+
private staticDirty=true;
|
|
46
|
+
private dynamicDirty=false;
|
|
47
|
+
private fittedBounds=new THREE.Box3();
|
|
48
|
+
private passes=0;
|
|
49
|
+
private staticPasses=0;
|
|
50
|
+
private dynamicPasses=0;
|
|
51
|
+
constructor(){this.mesh.frustumCulled=false;this.scene.add(this.mesh);}
|
|
52
|
+
|
|
53
|
+
sync(objects:Iterable<THREE.Object3D>,world:THREE.Texture|null,root:THREE.Object3D,enabled:boolean):void {
|
|
54
|
+
this.uniforms.blenderWorldVisibilityEnabled.value=enabled && world!==null;
|
|
55
|
+
this.uniforms.blenderWorldRadiance.value=world;
|
|
56
|
+
this.uniforms.blenderWorldToNative.value.setFromMatrix4(root.matrixWorld).invert();
|
|
57
|
+
if(!enabled || !world)return;
|
|
58
|
+
const meshes=[...objects].filter((object):object is THREE.Mesh=>(object as THREE.Mesh).isMesh===true &&
|
|
59
|
+
!!(object as THREE.Mesh).geometry.getAttribute('position'));
|
|
60
|
+
const layoutChanged=meshes.length!==this.segments.length || meshes.some((mesh,i)=>{
|
|
61
|
+
const previous=this.segments[i],geometry=mesh.geometry;
|
|
62
|
+
return !previous || previous.mesh!==mesh || previous.geometry!==geometry ||
|
|
63
|
+
previous.position!==geometry.getAttribute('position') || previous.index!==geometry.index ||
|
|
64
|
+
previous.start!==geometry.drawRange.start || previous.count!==Math.min(geometry.drawRange.count,
|
|
65
|
+
(geometry.index?.count ?? geometry.getAttribute('position').count)-geometry.drawRange.start);
|
|
66
|
+
});
|
|
67
|
+
if(layoutChanged) {
|
|
68
|
+
for(const mesh of this.dynamicMeshes.values()){mesh.removeFromParent();mesh.geometry.dispose();}
|
|
69
|
+
this.dynamicMeshes.clear();this.dynamicDirty=true;this.fittedBounds.makeEmpty();
|
|
70
|
+
let offset=0;
|
|
71
|
+
this.segments=meshes.map(mesh=>{
|
|
72
|
+
const geometry=mesh.geometry,position=geometry.getAttribute('position'),index=geometry.index;
|
|
73
|
+
const start=geometry.drawRange.start,count=Math.max(0,Math.min(geometry.drawRange.count,(index?.count??position.count)-start));
|
|
74
|
+
const segment={mesh,geometry,position,index,positionVersion:-1,indexVersion:-1,start,count,
|
|
75
|
+
offset,matrix:new THREE.Matrix4(),shown:!shown(mesh),bounds:new THREE.Box3(),dynamic:false};
|
|
76
|
+
offset+=count;return segment;
|
|
77
|
+
});
|
|
78
|
+
this.geometry.setAttribute('position',new THREE.BufferAttribute(new Float32Array(offset*3),3));
|
|
79
|
+
this.dirty=true;this.staticDirty=true;
|
|
80
|
+
}
|
|
81
|
+
const attribute=this.geometry.getAttribute('position') as THREE.BufferAttribute|undefined;
|
|
82
|
+
if(!attribute)return;
|
|
83
|
+
const point=new THREE.Vector3();
|
|
84
|
+
for(const segment of this.segments) {
|
|
85
|
+
const visible=shown(segment.mesh),pv=version(segment.position),iv=segment.index?.version??0;
|
|
86
|
+
if(segment.shown===visible && segment.matrix.equals(segment.mesh.matrixWorld) &&
|
|
87
|
+
segment.positionVersion===pv && segment.indexVersion===iv)continue;
|
|
88
|
+
// Once an object moves/deforms, keep it in the small dynamic atlas. Its
|
|
89
|
+
// later poses must never redraw every static triangle in the environment.
|
|
90
|
+
if(!segment.dynamic && segment.positionVersion>=0 &&
|
|
91
|
+
(!segment.matrix.equals(segment.mesh.matrixWorld) || segment.positionVersion!==pv || segment.indexVersion!==iv)) {
|
|
92
|
+
const geometry=new THREE.BufferGeometry();
|
|
93
|
+
geometry.setAttribute('position',new THREE.BufferAttribute(new Float32Array(segment.count*3),3));
|
|
94
|
+
const mesh=new THREE.Mesh(geometry,this.material);mesh.frustumCulled=false;
|
|
95
|
+
this.dynamicScene.add(mesh);this.dynamicMeshes.set(segment.mesh,mesh);segment.dynamic=true;
|
|
96
|
+
(attribute.array as Float32Array).fill(0,segment.offset*3,(segment.offset+segment.count)*3);
|
|
97
|
+
attribute.addUpdateRange(segment.offset*3,segment.count*3);attribute.needsUpdate=true;this.staticDirty=true;
|
|
98
|
+
}
|
|
99
|
+
const dynamicMesh=this.dynamicMeshes.get(segment.mesh);
|
|
100
|
+
const written=(dynamicMesh?.geometry.getAttribute('position')??attribute) as THREE.BufferAttribute;
|
|
101
|
+
if(dynamicMesh)dynamicMesh.visible=visible;
|
|
102
|
+
segment.bounds.makeEmpty();
|
|
103
|
+
for(let i=0;i<segment.count;i++) {
|
|
104
|
+
if(visible) {
|
|
105
|
+
const vertex=segment.index?.getX(segment.start+i) ?? segment.start+i;
|
|
106
|
+
point.fromBufferAttribute(segment.position,vertex).applyMatrix4(segment.mesh.matrixWorld);
|
|
107
|
+
segment.bounds.expandByPoint(point);
|
|
108
|
+
} else point.set(0,0,0); // Degenerate the hidden range without changing its layout.
|
|
109
|
+
written.setXYZ(dynamicMesh?i:segment.offset+i,point.x,point.y,point.z);
|
|
110
|
+
}
|
|
111
|
+
written.addUpdateRange(dynamicMesh?0:segment.offset*3,segment.count*3);written.needsUpdate=true;
|
|
112
|
+
if(dynamicMesh)this.dynamicDirty=true;else this.staticDirty=true;
|
|
113
|
+
segment.shown=visible;segment.matrix.copy(segment.mesh.matrixWorld);
|
|
114
|
+
segment.positionVersion=pv;segment.indexVersion=iv;this.dirty=true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Draw the atlas only when a caster changes. All directional samples share
|
|
119
|
+
* one texture sampler and one merged depth draw, retaining offscreen casters. */
|
|
120
|
+
prepare(renderer:THREE.WebGLRenderer):void {
|
|
121
|
+
if(!this.uniforms.blenderWorldVisibilityEnabled.value || !this.dirty)return;
|
|
122
|
+
if(!this.target) {
|
|
123
|
+
const size=TILES*TILE_SIZE;
|
|
124
|
+
this.target=new THREE.WebGLRenderTarget(size,size,{depthBuffer:true,stencilBuffer:false});
|
|
125
|
+
this.target.depthTexture=new THREE.DepthTexture(size,size,THREE.UnsignedIntType);
|
|
126
|
+
this.target.depthTexture.minFilter=this.target.depthTexture.magFilter=THREE.NearestFilter;
|
|
127
|
+
this.uniforms.blenderWorldDepth.value=this.target.depthTexture;
|
|
128
|
+
}
|
|
129
|
+
if(this.dynamicMeshes.size && !this.dynamicTarget) {
|
|
130
|
+
const size=TILES*TILE_SIZE;
|
|
131
|
+
this.dynamicTarget=new THREE.WebGLRenderTarget(size,size,{depthBuffer:true,stencilBuffer:false});
|
|
132
|
+
this.dynamicTarget.depthTexture=new THREE.DepthTexture(size,size,THREE.UnsignedIntType);
|
|
133
|
+
this.dynamicTarget.depthTexture.minFilter=this.dynamicTarget.depthTexture.magFilter=THREE.NearestFilter;
|
|
134
|
+
this.uniforms.blenderWorldDynamicDepth.value=this.dynamicTarget.depthTexture;
|
|
135
|
+
}
|
|
136
|
+
this.uniforms.blenderWorldHasDynamicDepth.value=this.dynamicMeshes.size>0;
|
|
137
|
+
const bounds=this.segments.reduce((all,segment)=>all.union(segment.bounds),new THREE.Box3());
|
|
138
|
+
if(this.fittedBounds.isEmpty() || !this.fittedBounds.containsBox(bounds)) {
|
|
139
|
+
this.fittedBounds.union(bounds);this.staticDirty=true;
|
|
140
|
+
}
|
|
141
|
+
const sphere=this.fittedBounds.getBoundingSphere(new THREE.Sphere());
|
|
142
|
+
this.mesh.visible=(this.geometry.getAttribute('position')?.count??0)>0;
|
|
143
|
+
const radius=Math.max(sphere.radius,1e-3),distance=radius*2+1;
|
|
144
|
+
this.uniforms.blenderWorldRayOffset.value=radius*1e-5;
|
|
145
|
+
this.camera.left=-radius;this.camera.right=radius;this.camera.top=radius;this.camera.bottom=-radius;
|
|
146
|
+
this.camera.near=.001;this.camera.far=distance+radius*2;
|
|
147
|
+
this.camera.updateProjectionMatrix();
|
|
148
|
+
const target=renderer.getRenderTarget(),autoClear=renderer.autoClear,shadowMap=renderer.shadowMap.enabled;
|
|
149
|
+
try {
|
|
150
|
+
renderer.autoClear=false;renderer.shadowMap.enabled=false;
|
|
151
|
+
const targets:[THREE.WebGLRenderTarget,THREE.Scene,boolean][]=[[this.target,this.scene,this.staticDirty]];
|
|
152
|
+
if(this.dynamicTarget)targets.push([this.dynamicTarget,this.dynamicScene,this.dynamicDirty||this.staticDirty]);
|
|
153
|
+
for(const [target,,draw] of targets)if(draw) {
|
|
154
|
+
target.viewport.set(0,0,TILES*TILE_SIZE,TILES*TILE_SIZE);target.scissorTest=false;
|
|
155
|
+
renderer.setRenderTarget(target);renderer.clear(true,true,false);
|
|
156
|
+
}
|
|
157
|
+
for(let i=0;i<SAMPLE_COUNT;i++) {
|
|
158
|
+
const direction=this.uniforms.blenderWorldDirections.value[i]!;
|
|
159
|
+
this.camera.position.copy(sphere.center).addScaledVector(direction,distance);
|
|
160
|
+
this.camera.up.set(0,1,0);this.camera.lookAt(sphere.center);this.camera.updateMatrixWorld(true);
|
|
161
|
+
this.uniforms.blenderWorldMatrices.value[i]!.multiplyMatrices(this.camera.projectionMatrix,this.camera.matrixWorldInverse);
|
|
162
|
+
const x=(i%TILES)*TILE_SIZE,y=Math.floor(i/TILES)*TILE_SIZE;
|
|
163
|
+
// Render-target viewports use device pixels. Renderer.setViewport uses
|
|
164
|
+
// logical pixels even offscreen, so a HiDPI view would overlap the tiles.
|
|
165
|
+
for(const [target,scene,draw] of targets)if(draw) {
|
|
166
|
+
target.viewport.set(x,y,TILE_SIZE,TILE_SIZE);target.scissor.set(x,y,TILE_SIZE,TILE_SIZE);target.scissorTest=true;
|
|
167
|
+
renderer.setRenderTarget(target);renderer.render(scene,this.camera);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if(this.staticDirty){this.passes+=SAMPLE_COUNT;this.staticPasses+=SAMPLE_COUNT;}
|
|
171
|
+
if(this.dynamicTarget && (this.dynamicDirty||this.staticDirty)){this.passes+=SAMPLE_COUNT;this.dynamicPasses+=SAMPLE_COUNT;}
|
|
172
|
+
this.dirty=false;this.staticDirty=false;this.dynamicDirty=false;
|
|
173
|
+
} finally {
|
|
174
|
+
renderer.setRenderTarget(target);renderer.autoClear=autoClear;renderer.shadowMap.enabled=shadowMap;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
diagnostics(){return {enabled:this.uniforms.blenderWorldVisibilityEnabled.value,world:!!this.uniforms.blenderWorldRadiance.value,
|
|
178
|
+
depth:!!this.uniforms.blenderWorldDepth.value,passes:this.passes,staticPasses:this.staticPasses,dynamicPasses:this.dynamicPasses,
|
|
179
|
+
dynamicObjects:this.dynamicMeshes.size,vertices:this.geometry.getAttribute('position')?.count??0,
|
|
180
|
+
casters:this.segments.filter(segment=>segment.shown).length,dirty:this.dirty};}
|
|
181
|
+
dispose():void {
|
|
182
|
+
this.uniforms.blenderWorldVisibilityEnabled.value=false;
|
|
183
|
+
this.target?.dispose();this.target=null;this.uniforms.blenderWorldDepth.value=null;
|
|
184
|
+
this.dynamicTarget?.dispose();this.dynamicTarget=null;this.uniforms.blenderWorldDynamicDepth.value=null;
|
|
185
|
+
this.uniforms.blenderWorldHasDynamicDepth.value=false;
|
|
186
|
+
for(const mesh of this.dynamicMeshes.values()){mesh.removeFromParent();mesh.geometry.dispose();}this.dynamicMeshes.clear();
|
|
187
|
+
this.geometry.dispose();this.material.dispose();this.segments=[];
|
|
188
|
+
this.geometry.deleteAttribute('position');this.dirty=true;this.staticDirty=true;this.dynamicDirty=false;this.fittedBounds.makeEmpty();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const bindings=new WeakMap<THREE.Material,BlenderWorldVisibility>();
|
|
193
|
+
const compiled=new WeakMap<THREE.Material,Set<THREE.WebGLProgramParametersWithUniforms['uniforms']>>();
|
|
194
|
+
const inactive=new BlenderWorldVisibility();
|
|
195
|
+
export function bindWorldVisibility(material:THREE.Material,visibility:BlenderWorldVisibility):void {
|
|
196
|
+
if(bindings.get(material)===visibility)return;
|
|
197
|
+
bindings.set(material,visibility);
|
|
198
|
+
for(const uniforms of compiled.get(material)??[])Object.assign(uniforms,visibility.uniforms);
|
|
199
|
+
}
|
|
200
|
+
export function copyWorldVisibility(source:THREE.Material,target:THREE.Material):void {
|
|
201
|
+
const visibility=bindings.get(source);if(visibility)bindWorldVisibility(target,visibility);
|
|
202
|
+
}
|
|
203
|
+
export function worldVisibilityUniforms(material:THREE.Material,uniforms:THREE.WebGLProgramParametersWithUniforms['uniforms']):void {
|
|
204
|
+
let held=compiled.get(material);if(!held){held=new Set();compiled.set(material,held);}held.add(uniforms);
|
|
205
|
+
Object.assign(uniforms,(bindings.get(material)??inactive).uniforms);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const worldVisibilityShader=/* glsl */`
|
|
209
|
+
varying vec3 blenderWorldPosition;
|
|
210
|
+
uniform bool blenderWorldVisibilityEnabled;
|
|
211
|
+
uniform sampler2D blenderWorldDepth, blenderWorldDynamicDepth, blenderWorldRadiance;
|
|
212
|
+
uniform bool blenderWorldHasDynamicDepth;
|
|
213
|
+
uniform vec3 blenderWorldDirections[16];
|
|
214
|
+
uniform mat4 blenderWorldMatrices[16];
|
|
215
|
+
uniform mat3 blenderWorldToNative;
|
|
216
|
+
uniform float blenderWorldRayOffset;
|
|
217
|
+
float blenderWorldVisible(int i, vec3 position, vec3 worldDx, vec3 worldDy) {
|
|
218
|
+
vec4 clip=blenderWorldMatrices[i]*vec4(position,1.);
|
|
219
|
+
vec3 point=clip.xyz/clip.w*.5+.5;
|
|
220
|
+
if(any(lessThan(point,vec3(0.))) || any(greaterThan(point,vec3(1.))))return 1.;
|
|
221
|
+
vec2 tile=vec2(float(i%4),float(i/4));
|
|
222
|
+
vec2 samplePoint=clamp((floor(point.xy*512.)+.5)/512.,vec2(.5/512.),vec2(1.-.5/512.));
|
|
223
|
+
vec2 uv=(tile+samplePoint)/4.;
|
|
224
|
+
float depth=texture2D(blenderWorldDepth,uv).x;
|
|
225
|
+
if(blenderWorldHasDynamicDepth)depth=min(depth,texture2D(blenderWorldDynamicDepth,uv).x);
|
|
226
|
+
// Compare the receiver plane AT THE DEPTH TEXEL, not at its nearby fragment.
|
|
227
|
+
// A fixed depth bias falsely shadows an unoccluded tilted plane; increasing
|
|
228
|
+
// that bias leaks through thin walls. Derivatives retain its actual slope.
|
|
229
|
+
vec3 dx=(blenderWorldMatrices[i]*vec4(worldDx,0.)).xyz*.5;
|
|
230
|
+
vec3 dy=(blenderWorldMatrices[i]*vec4(worldDy,0.)).xyz*.5;
|
|
231
|
+
float determinant=dx.x*dy.y-dx.y*dy.x;
|
|
232
|
+
vec2 gradient=abs(determinant)>1e-12 ? vec2(dx.z*dy.y-dy.z*dx.y,dy.z*dx.x-dx.z*dy.x)/determinant : vec2(0.);
|
|
233
|
+
float receiverDepth=point.z+dot(gradient,samplePoint-point.xy);
|
|
234
|
+
return receiverDepth<=depth+1e-6 ? 1. : 0.;
|
|
235
|
+
}
|
|
236
|
+
vec3 blenderWorldIrradiance(vec3 normal, vec3 fallbackIrradiance) {
|
|
237
|
+
if(!blenderWorldVisibilityEnabled)return fallbackIrradiance;
|
|
238
|
+
vec3 n=inverseTransformDirection(normal,viewMatrix),sum=vec3(0.);
|
|
239
|
+
float weights=0.;vec3 origin=blenderWorldPosition+n*blenderWorldRayOffset;
|
|
240
|
+
vec3 worldDx=dFdx(blenderWorldPosition),worldDy=dFdy(blenderWorldPosition);
|
|
241
|
+
for(int i=0;i<16;i++) {
|
|
242
|
+
vec3 direction=blenderWorldDirections[i];float weight=max(0.,dot(n,direction));weights+=weight;
|
|
243
|
+
if(weight>0. && blenderWorldVisible(i,origin,worldDx,worldDy)>0.) {
|
|
244
|
+
vec3 nativeDirection=normalize(blenderWorldToNative*direction);
|
|
245
|
+
vec2 uv=vec2(atan(-nativeDirection.y,nativeDirection.x)/6.28318530718+.5,
|
|
246
|
+
asin(clamp(nativeDirection.z,-1.,1.))/3.14159265359+.5);
|
|
247
|
+
sum+=texture2D(blenderWorldRadiance,uv).rgb*weight;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return sum*(3.14159265359/max(weights,1e-8));
|
|
251
|
+
}
|
|
252
|
+
float blenderWorldReflectionVisibility(vec3 viewDirection,vec3 normal,float roughness) {
|
|
253
|
+
if(!blenderWorldVisibilityEnabled)return 1.;
|
|
254
|
+
vec3 n=inverseTransformDirection(normal,viewMatrix),r=inverseTransformDirection(reflect(-viewDirection,normal),viewMatrix);
|
|
255
|
+
vec3 origin=blenderWorldPosition+n*blenderWorldRayOffset;
|
|
256
|
+
vec3 worldDx=dFdx(blenderWorldPosition),worldDy=dFdy(blenderWorldPosition);
|
|
257
|
+
float sum=0.,weights=0.;
|
|
258
|
+
for(int i=0;i<16;i++) {
|
|
259
|
+
float weight=pow(max(0.,dot(r,blenderWorldDirections[i])),mix(64.,1.,roughness));
|
|
260
|
+
weights+=weight;sum+=weight*blenderWorldVisible(i,origin,worldDx,worldDy);
|
|
261
|
+
}
|
|
262
|
+
return sum/max(weights,1e-8);
|
|
263
|
+
}
|
|
264
|
+
`;
|
|
@@ -18,7 +18,7 @@ import { gradientTexture } from './blender-gradient-texture';
|
|
|
18
18
|
// Type-only, so this module keeps NO runtime edge back to the contribution
|
|
19
19
|
// that uses it -- which is what lets a worker import it alone.
|
|
20
20
|
import type { WorldExpression } from './blender-runtime-lighting';
|
|
21
|
-
import { type SkyParameters, skyField } from './blender-sky';
|
|
21
|
+
import { type SkyParameters, skyField, skySunDirection } from './blender-sky';
|
|
22
22
|
import { worldMath } from './world-math';
|
|
23
23
|
|
|
24
24
|
export function worldField(
|
|
@@ -101,6 +101,9 @@ export function worldField(
|
|
|
101
101
|
// rebuilding it per direction would be a 65,536-pixel job per texel.
|
|
102
102
|
const field = skyField({
|
|
103
103
|
...(expression.sky_model === undefined ? {} : {model: expression.sky_model}),
|
|
104
|
+
...(expression.sun_disc === undefined ? {} : {sunDisc: expression.sun_disc}),
|
|
105
|
+
...(expression.sun_size === undefined ? {} : {sunSize: expression.sun_size}),
|
|
106
|
+
...(expression.sun_intensity === undefined ? {} : {sunIntensity: expression.sun_intensity}),
|
|
104
107
|
sunElevation: expression.sun_elevation,
|
|
105
108
|
sunRotation: expression.sun_rotation,
|
|
106
109
|
altitude: expression.altitude,
|
|
@@ -255,6 +258,9 @@ export function collectSkyParameters(expression: WorldExpression): SkyParameters
|
|
|
255
258
|
case 'sky':
|
|
256
259
|
found.push({
|
|
257
260
|
...(node.sky_model === undefined ? {} : {model: node.sky_model}),
|
|
261
|
+
...(node.sun_disc === undefined ? {} : {sunDisc: node.sun_disc}),
|
|
262
|
+
...(node.sun_size === undefined ? {} : {sunSize: node.sun_size}),
|
|
263
|
+
...(node.sun_intensity === undefined ? {} : {sunIntensity: node.sun_intensity}),
|
|
258
264
|
sunElevation: node.sun_elevation,
|
|
259
265
|
sunRotation: node.sun_rotation,
|
|
260
266
|
altitude: node.altitude,
|
|
@@ -298,6 +304,57 @@ export function collectSkyParameters(expression: WorldExpression): SkyParameters
|
|
|
298
304
|
return found;
|
|
299
305
|
}
|
|
300
306
|
|
|
307
|
+
/** Keep the broad sky in IBL and integrate its small bright disc separately.
|
|
308
|
+
* A 256-wide environment map cannot resolve a one-degree sun reliably, and
|
|
309
|
+
* IBL has no geometric shadow visibility. A directional shadow light carries
|
|
310
|
+
* the disc's integrated irradiance; it is an angular-size approximation,
|
|
311
|
+
* rather than a replacement for Cycles' finite-disc path sampling. */
|
|
312
|
+
export function withoutSolarDiscs(expression: WorldExpression): WorldExpression {
|
|
313
|
+
return JSON.parse(JSON.stringify(expression), (_key, value) =>
|
|
314
|
+
value?.kind === 'sky' && value.sky_model === 'SINGLE_SCATTERING'
|
|
315
|
+
? {...value, sun_disc: false} : value) as WorldExpression;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function worldSolarIrradiance(expression: WorldExpression): {direction: THREE.Vector3; irradiance: THREE.Vector3}[] {
|
|
319
|
+
const suns = collectSkyParameters(expression).filter(p => p.model === 'SINGLE_SCATTERING' &&
|
|
320
|
+
p.sunDisc && (p.sunSize ?? 0) > 0 && (p.sunIntensity ?? 1) > 0);
|
|
321
|
+
if (!suns.length) return [];
|
|
322
|
+
// Window-dependent world shaders need ray origins as well as directions;
|
|
323
|
+
// there is no position-independent directional light for that field.
|
|
324
|
+
if (usesWindowCoordinates(expression)) return [];
|
|
325
|
+
const diffuse = worldField(withoutSolarDiscs(expression));
|
|
326
|
+
const unique = [...new Map(suns.map(p => [JSON.stringify([p.sunElevation, p.sunRotation, p.sunSize]), p])).values()];
|
|
327
|
+
return unique.map(p => {
|
|
328
|
+
// Other discs must not be counted again when they overlap this one's
|
|
329
|
+
// integration domain. Equal domains are one group, including mixed nodes.
|
|
330
|
+
const complete = worldField(JSON.parse(JSON.stringify(expression), (_key, value) =>
|
|
331
|
+
value?.kind === 'sky' && value.sky_model === 'SINGLE_SCATTERING' &&
|
|
332
|
+
(value.sun_elevation !== p.sunElevation || value.sun_rotation !== p.sunRotation || value.sun_size !== p.sunSize)
|
|
333
|
+
? {...value,sun_disc:false} : value) as WorldExpression);
|
|
334
|
+
const direction = skySunDirection(p);
|
|
335
|
+
const u = new THREE.Vector3().crossVectors(direction, Math.abs(direction.z) < .9
|
|
336
|
+
? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 1, 0)).normalize();
|
|
337
|
+
const v = new THREE.Vector3().crossVectors(direction, u);
|
|
338
|
+
const irradiance = new THREE.Vector3(), ray = new THREE.Vector3(), delta = new THREE.Vector3();
|
|
339
|
+
const solidAngle = 2 * Math.PI * (1 - Math.cos(p.sunSize! / 2));
|
|
340
|
+
const radial = 32, azimuthal = 32;
|
|
341
|
+
for (let i = 0; i < radial; i++) {
|
|
342
|
+
const cos = 1 - (i + .5) / radial * (1 - Math.cos(p.sunSize! / 2));
|
|
343
|
+
const sin = Math.sqrt(1 - cos * cos);
|
|
344
|
+
for (let j = 0; j < azimuthal; j++) {
|
|
345
|
+
const phi = (j + .5) / azimuthal * 2 * Math.PI;
|
|
346
|
+
ray.copy(direction).multiplyScalar(cos).addScaledVector(u, sin * Math.cos(phi)).addScaledVector(v, sin * Math.sin(phi));
|
|
347
|
+
const all = complete(ray);
|
|
348
|
+
if (typeof all === 'number') delta.setScalar(all); else delta.copy(all);
|
|
349
|
+
const base = diffuse(ray);
|
|
350
|
+
if (typeof base === 'number') delta.addScalar(-base); else delta.sub(base);
|
|
351
|
+
irradiance.addScaledVector(delta, cos * solidAngle / (radial * azimuthal));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return {direction, irradiance};
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
301
358
|
/** Whether this expression depends on the camera's normalized image coordinates. */
|
|
302
359
|
export function usesWindowCoordinates(expression: WorldExpression): boolean {
|
|
303
360
|
if (typeof expression === 'number' || Array.isArray(expression)) return false;
|
package/browser/volter_three.py
CHANGED
|
@@ -225,7 +225,11 @@ def _export():
|
|
|
225
225
|
"known": {},
|
|
226
226
|
"buffer_path": EXPORT_BUFFER_PATH,
|
|
227
227
|
}
|
|
228
|
-
|
|
228
|
+
# The native door reads UTF-8 and does not decode `\\uXXXX` escapes (it
|
|
229
|
+
# drops the backslash), so the request goes unescaped, as session.py's door
|
|
230
|
+
# calls send theirs: a non-ASCII presenter root in `buffer_path` would
|
|
231
|
+
# otherwise reach the door misspelled.
|
|
232
|
+
frame = json.loads(_blender_web.export_frame(json.dumps(options, ensure_ascii=False)))
|
|
229
233
|
error = frame.get("error")
|
|
230
234
|
if error:
|
|
231
235
|
raise RuntimeError("Blender export door: %s" % error)
|
package/browser/worker.ts
CHANGED
|
@@ -577,6 +577,43 @@ async function stageProjectFiles(files_: BlenderFiles, project: string): Promise
|
|
|
577
577
|
return failed;
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
+
/** What `session.py::follow_open_file` says after a script changed the file Blender holds. */
|
|
581
|
+
type FollowedDocument =
|
|
582
|
+
| { moved: true; from: string; document: string }
|
|
583
|
+
| { moved: false; from: string; outside: string };
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* THE DOCUMENT FOLLOWS BLENDER'S SAVE AS, and the script's caller is told in its own answer.
|
|
587
|
+
*
|
|
588
|
+
* A move retargets the session's saves here (the save this call ends with lands the new file
|
|
589
|
+
* in the project, which is where the editor's Model tab for it opens from) and tells the tab,
|
|
590
|
+
* whose runtime handle holds the same path. A file outside the project is refused: the editor
|
|
591
|
+
* has no document there, so the session keeps its own and keeps saving to it, and says so
|
|
592
|
+
* naming both files, since the script believes it is now working in the other one.
|
|
593
|
+
*/
|
|
594
|
+
function followDocument(followed: FollowedDocument | undefined): string {
|
|
595
|
+
if (!followed) return '';
|
|
596
|
+
if (!followed.moved) {
|
|
597
|
+
const line = `Blender now has ${followed.outside} open, which is outside the project, so the editor cannot ` +
|
|
598
|
+
`follow it there: this session's document stays ${followed.from}, and the model keeps saving to ` +
|
|
599
|
+
`${followed.from}. To move the model to another file, save it inside the project (src/models/<name>.blend).`;
|
|
600
|
+
log('warn', line);
|
|
601
|
+
return `\n\n${line}`;
|
|
602
|
+
}
|
|
603
|
+
if (!isDocumentPath(followed.document)) {
|
|
604
|
+
// The session checks the same shape, so this is a broken promise, not a refusal to explain.
|
|
605
|
+
throw new Error(`Blender moved its document to ${JSON.stringify(followed.document)}, which is not a project .blend path`);
|
|
606
|
+
}
|
|
607
|
+
documentPath = followed.document;
|
|
608
|
+
// Even a present that failed must not leave the new file only in the engine's filesystem.
|
|
609
|
+
setDocumentDirty(true);
|
|
610
|
+
post({ op: 'document-moved', from: followed.from, document: followed.document });
|
|
611
|
+
const line = `Blender now has ${followed.document} open (this code saved or opened it), so the editor follows it: ` +
|
|
612
|
+
`the model saves to ${followed.document} from now on, and ${followed.from} keeps what it last saved.`;
|
|
613
|
+
log('log', `@@VOLTER-DOCUMENT-MOVED ${JSON.stringify({ from: followed.from, document: followed.document })}`);
|
|
614
|
+
return `\n\n${line}`;
|
|
615
|
+
}
|
|
616
|
+
|
|
580
617
|
/** The failures `stageProjectFiles` answered, as one paragraph a script's caller can read. */
|
|
581
618
|
function stagingNote(failed: readonly string[]): string {
|
|
582
619
|
return failed.length === 0 ? '' :
|
|
@@ -678,14 +715,22 @@ async function handle(request: WorkerRequest): Promise<unknown> {
|
|
|
678
715
|
const note = stagingNote(await stageProjectFiles(files, projectRoot));
|
|
679
716
|
const answer = await ask({ op: 'execute', code: request.code, history: request.history ?? true,
|
|
680
717
|
label: request.label ?? 'Blender Python' }) as {
|
|
681
|
-
error?: string; result: string;
|
|
718
|
+
error?: string; result: string; document?: FollowedDocument;
|
|
682
719
|
};
|
|
683
|
-
return (answer.error ? `Error executing code: ${answer.error}` : `Code executed successfully: ${answer.result}`) +
|
|
720
|
+
return (answer.error ? `Error executing code: ${answer.error}` : `Code executed successfully: ${answer.result}`) +
|
|
721
|
+
note + followDocument(answer.document);
|
|
684
722
|
}
|
|
685
723
|
case 'present':
|
|
686
724
|
// Straight through to `session.py`'s own `present` op — the worker adds
|
|
687
725
|
// nothing, and a capture-less present answers `{ presented, revision }`.
|
|
688
726
|
return pullWork('present', () => ask({ op: 'present' }));
|
|
727
|
+
case 'native-preview':
|
|
728
|
+
return ask({ op: 'native-preview', width: request.width, height: request.height,
|
|
729
|
+
samples: request.samples, ...(request.camera === undefined ? {} : { camera: request.camera }) });
|
|
730
|
+
case 'list-files-snapshot': {
|
|
731
|
+
const document = await ask({ op: 'document-info' });
|
|
732
|
+
return { entries: await listSessionFiles(files, request.path), document };
|
|
733
|
+
}
|
|
689
734
|
case 'scene-info':
|
|
690
735
|
return session.sceneInfo();
|
|
691
736
|
case 'object-info':
|
package/package.json
CHANGED
package/wasm/BUNDLE.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"blender": "5.2.0 LTS",
|
|
3
|
-
"source": "
|
|
3
|
+
"source": "434072350c0577eb155068d5a1b62bb45daf19f9",
|
|
4
4
|
"upstream": "fbe6228777e7d9afefcd61a413844e790ae75db7",
|
|
5
5
|
"sourceRepository": "https://github.com/volter-ai/blender",
|
|
6
|
-
"sourceRecipe": "https://github.com/volter-ai/blender/blob/
|
|
6
|
+
"sourceRecipe": "https://github.com/volter-ai/blender/blob/434072350c0577eb155068d5a1b62bb45daf19f9/build_files/emscripten/RELEASE.md",
|
|
7
7
|
"factoryEngine": "BLENDER_EEVEE",
|
|
8
8
|
"files": {
|
|
9
9
|
"blender_browser.js": "raw",
|
|
@@ -13,38 +13,38 @@
|
|
|
13
13
|
"rawFiles": {
|
|
14
14
|
"blender_browser.js": {
|
|
15
15
|
"bytes": 408423,
|
|
16
|
-
"sha256": "
|
|
16
|
+
"sha256": "d818e8d746bc62ebf810206ac564d2dc75dc645b3121f4a3919d13ecef11e419"
|
|
17
17
|
},
|
|
18
18
|
"blender_browser.wasm": {
|
|
19
|
-
"bytes":
|
|
20
|
-
"sha256": "
|
|
19
|
+
"bytes": 86344416,
|
|
20
|
+
"sha256": "f5b988d30a9f29708254bc84600f7b88d9c9eb41af904b84c9f398d5c41595e4"
|
|
21
21
|
},
|
|
22
22
|
"blender_browser.data": {
|
|
23
|
-
"bytes":
|
|
24
|
-
"sha256": "
|
|
23
|
+
"bytes": 52699590,
|
|
24
|
+
"sha256": "89cfee5131d8a7c7361b1f8d249fb08a81b14102963ae161e1146eabab56e558"
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"shippedFiles": {
|
|
28
28
|
"blender_browser.js": {
|
|
29
29
|
"bytes": 408423,
|
|
30
|
-
"sha256": "
|
|
30
|
+
"sha256": "d818e8d746bc62ebf810206ac564d2dc75dc645b3121f4a3919d13ecef11e419"
|
|
31
31
|
},
|
|
32
32
|
"blender_browser.wasm.br": {
|
|
33
|
-
"bytes":
|
|
34
|
-
"sha256": "
|
|
33
|
+
"bytes": 18611942,
|
|
34
|
+
"sha256": "b415a2fce0f2d2f796a2a3362b38420df063866898cc04c61d4604c5caf13e0d"
|
|
35
35
|
},
|
|
36
36
|
"blender_browser.data.br": {
|
|
37
|
-
"bytes":
|
|
38
|
-
"sha256": "
|
|
37
|
+
"bytes": 8056485,
|
|
38
|
+
"sha256": "899d53d55f730eb12602b23c9f78e034e45e244380719d310ee11098725911d8"
|
|
39
39
|
},
|
|
40
40
|
"DEPENDENCY-LICENSES.txt": {
|
|
41
41
|
"bytes": 241352,
|
|
42
42
|
"sha256": "4085e9336266cf66f061d4d5c962a5929cc96d5732c746b5c13ea05cd104a75b"
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
|
-
"note": "Stoneguard c7/c14/c15 engine. Emscripten 6.0.5, Release -O3 objects and -O2 link. Portable native arm64 matrix reciprocal plus fused generic OpenSubdiv stencil and patch accumulation; bodice four-stage comparison 28/28 arrays exact. Full-scene comparison and memory scope recorded in provenance/stoneguard-memory.json. Historical battery absent; no full-battery or hosted acceptance claim.",
|
|
45
|
+
"note": "Stoneguard c7/c14/c15 engine. Emscripten 6.0.5, Release -O3 objects and -O2 link. Portable native arm64 matrix reciprocal plus fused generic OpenSubdiv stencil and patch accumulation; bodice four-stage comparison 28/28 arrays exact. Full-scene comparison and memory scope recorded in provenance/stoneguard-memory.json. Historical battery absent; no full-battery or hosted acceptance claim. Rebuilt from the public instance-source commit with the same Emscripten 6.0.5, Release -O3 objects and -O2 link. All 6287 drawable Lone Monk placements resolve to 26 native source objects and 24 deferred mesh resources. Live raster rendering still differs from native Cycles; no full-scene visual parity claim. Rebuilt from the public evaluated-geometry identity commit with the same Emscripten 6.0.5 Release recipe. Distinct Array/Mirror/deformation outputs no longer borrow another object’s geometry. Native linked-input sharing and every deferred probe vertex are checked independently. Live raster illumination remains unoccluded environment lighting and differs substantially from native Cycles; no visual parity claim.",
|
|
46
46
|
"callTrampoline": "CPython 3.13.13 is compiled with PY_CALL_TRAMPOLINE for Blender CPython slot arity casts. This applies to CPython dispatch, not arbitrary indirect calls.",
|
|
47
|
-
"engineIds": "The
|
|
48
|
-
"exportDoor": "The source-owned bpy_web_export.cc exports Principled physical inputs, normal images, eight named UV layers and evaluated corner normals, and takes graph_materials/graph_images/graph_generated (a graph-drawn shader mix reduced to the one Principled BSDF it reaches, graph images at the door revisions with every UDIM tile, orco for deformed meshes whose graph reads Generated coordinates). A frame can defer its changed meshes and pictures (defer) for the caller to pull one at a time (export_mesh, export_image), each call freeing the previous one's columns and a picture it decoded; an undo step keeps the revisions when Blender kept the depsgraph (session_undo); Blender's CPU subdivision descriptor is released once a mesh is read; the memory door reports malloc in-use and free. Datablock revisions are keyed by session_uid. writefile.cc writes one copy of the document per save in the browser build. CPU evaluators adopt stencil tables without cloning; browser subdivision descriptors are released immediately after CPU tessellation/deformation. Native evaluated corner triangles accompany corner normals. Consumed local conversion stencils are released before later UV channels are constructed. Caller-thread file-read, evaluated-object and subdivision checkpoints provide backpressure for resumable startup. Shared geometry revisions advance once per frame. Existing synchronous export remains available.",
|
|
47
|
+
"engineIds": "The session preserves the compiled native CYCLES engine. VOLTER_THREE selects the explicit raster presenter; BLENDER_EEVEE and BLENDER_WORKBENCH remain presentation engines.",
|
|
48
|
+
"exportDoor": "The source-owned bpy_web_export.cc exports Principled physical inputs, normal images, eight named UV layers and evaluated corner normals, and takes graph_materials/graph_images/graph_generated (a graph-drawn shader mix reduced to the one Principled BSDF it reaches, graph images at the door revisions with every UDIM tile, orco for deformed meshes whose graph reads Generated coordinates). A frame can defer its changed meshes and pictures (defer) for the caller to pull one at a time (export_mesh, export_image), each call freeing the previous one's columns and a picture it decoded; an undo step keeps the revisions when Blender kept the depsgraph (session_undo); Blender's CPU subdivision descriptor is released once a mesh is read; the memory door reports malloc in-use and free. Datablock revisions are keyed by session_uid. writefile.cc writes one copy of the document per save in the browser build. CPU evaluators adopt stencil tables without cloning; browser subdivision descriptors are released immediately after CPU tessellation/deformation. Native evaluated corner triangles accompany corner normals. Consumed local conversion stencils are released before later UV channels are constructed. Caller-thread file-read, evaluated-object and subdivision checkpoints provide backpressure for resumable startup. Shared geometry revisions advance once per frame. Existing synchronous export remains available. Optional instance_sources exports stable evaluated collection and particle source meshes, including excluded library meshes and converted curves. Frame-local instance_geometry tokens verify Python placement metadata against the exported geometry and are consumed before presentation. Geometry resource identity preserves implicit input storage only for unmodified geometry and separates evaluated modifier/deformation outputs. Object and mesh resource keys have independent namespaces.",
|
|
49
49
|
"preloadedPayload": "releasePreloadedFileData releases the JS preload buffer after session ready. The measured working payload exclusions are retained."
|
|
50
50
|
}
|
|
Binary file
|