@volter/blender-engine 0.1.95 → 0.1.97
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/session.py +18 -8
- package/browser/three/blender-runtime-view.ts +99 -10
- package/package.json +1 -1
package/browser/session.py
CHANGED
|
@@ -806,6 +806,8 @@ def _saved_view():
|
|
|
806
806
|
"distance": float(region.view_distance),
|
|
807
807
|
"perspective": region.view_perspective,
|
|
808
808
|
"shading": space.shading.type,
|
|
809
|
+
"scene_world": bool(space.shading.use_scene_world),
|
|
810
|
+
"scene_lights": bool(space.shading.use_scene_lights),
|
|
809
811
|
}
|
|
810
812
|
return None
|
|
811
813
|
|
|
@@ -4901,13 +4903,12 @@ def rna_outliner(selected=None):
|
|
|
4901
4903
|
# `add_layer_collections_recursive`: an EXCLUDED collection's objects
|
|
4902
4904
|
# are not added at all -- it is not in the view layer.
|
|
4903
4905
|
if not lc.exclude:
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
+
# Fold parenting before paging: a parent may follow its children in
|
|
4907
|
+
# collection order, and must not disappear behind the flat cap.
|
|
4908
|
+
for obj in collection.objects:
|
|
4906
4909
|
object_row = _outliner_object(obj, view_layer, seen, chosen, active)
|
|
4907
4910
|
place(obj, object_row, row)
|
|
4908
4911
|
children.append(object_row)
|
|
4909
|
-
if more:
|
|
4910
|
-
row["more"] = more
|
|
4911
4912
|
row["children"] = children
|
|
4912
4913
|
return row
|
|
4913
4914
|
|
|
@@ -4920,13 +4921,10 @@ def rna_outliner(selected=None):
|
|
|
4920
4921
|
_outliner_key("%s.layer_collection.children" % view_layer_path,
|
|
4921
4922
|
child.collection.name))
|
|
4922
4923
|
for child in root.children]
|
|
4923
|
-
|
|
4924
|
-
for obj in page:
|
|
4924
|
+
for obj in root.collection.objects:
|
|
4925
4925
|
object_row = _outliner_object(obj, view_layer, seen, chosen, active)
|
|
4926
4926
|
place(obj, object_row, scene_collection)
|
|
4927
4927
|
children.append(object_row)
|
|
4928
|
-
if more:
|
|
4929
|
-
scene_collection["more"] = more
|
|
4930
4928
|
scene_collection["children"] = children
|
|
4931
4929
|
|
|
4932
4930
|
# `ObjectsChildrenBuilder::make_object_parent_hierarchy_collections`, parents
|
|
@@ -4974,6 +4972,18 @@ def rna_outliner(selected=None):
|
|
|
4974
4972
|
parent_row["children"].append(duplicate)
|
|
4975
4973
|
mine.append([duplicate, parent_row, parent_collection])
|
|
4976
4974
|
|
|
4975
|
+
# Cap the hierarchy's visible child lists, not the collection's flat input.
|
|
4976
|
+
# Every parent participates in folding even when its child list needs a page.
|
|
4977
|
+
def page_tree(row):
|
|
4978
|
+
children, more = _outliner_page(row.get("children", []))
|
|
4979
|
+
row["children"] = children
|
|
4980
|
+
if more:
|
|
4981
|
+
row["more"] = more
|
|
4982
|
+
for child in children:
|
|
4983
|
+
page_tree(child)
|
|
4984
|
+
|
|
4985
|
+
page_tree(scene_collection)
|
|
4986
|
+
|
|
4977
4987
|
return {
|
|
4978
4988
|
"scene": scene_path,
|
|
4979
4989
|
"viewLayer": view_layer_path,
|
|
@@ -576,6 +576,8 @@ export const frameSchema = z
|
|
|
576
576
|
perspective: z.enum(['PERSP', 'ORTHO', 'CAMERA']),
|
|
577
577
|
lens: z.number().finite().positive().default(50),
|
|
578
578
|
shading: z.enum(['WIREFRAME', 'SOLID', 'MATERIAL', 'RENDERED']).optional(),
|
|
579
|
+
scene_world: z.boolean().optional(),
|
|
580
|
+
scene_lights: z.boolean().optional(),
|
|
579
581
|
})
|
|
580
582
|
.nullable()
|
|
581
583
|
.optional(),
|
|
@@ -693,9 +695,18 @@ export class BlenderRuntimeView {
|
|
|
693
695
|
private readonly lightExtrasRoot = new THREE.Group();
|
|
694
696
|
private readonly emptyExtrasRoot = new THREE.Group();
|
|
695
697
|
private rendered = false;
|
|
698
|
+
private sceneLights = false;
|
|
699
|
+
private shadowFit: {
|
|
700
|
+
camera: THREE.Camera | undefined;
|
|
701
|
+
cameraMatrix: THREE.Matrix4 | undefined;
|
|
702
|
+
projection: THREE.Matrix4 | undefined;
|
|
703
|
+
objects: Map<THREE.Object3D, { matrix: THREE.Matrix4; visible: boolean }>;
|
|
704
|
+
} | null = null;
|
|
696
705
|
/** The camera the VIEWPORT is held in render lighting through (Blender's Rendered shading),
|
|
697
706
|
* or null when the viewport shows modeling lighting. See {@link holdRendered}. */
|
|
698
707
|
private heldRendered: (() => THREE.Camera) | null = null;
|
|
708
|
+
/** Material Preview can use scene lighting while retaining viewport visibility. */
|
|
709
|
+
private heldVisibility: 'viewport' | 'render' = 'render';
|
|
699
710
|
/** What the World was last composed for ({@link worldKeyFor}), or null when it is cleared:
|
|
700
711
|
* composing rebuilds its textures, so it happens only when the key changes. */
|
|
701
712
|
private worldApplied: string | null = null;
|
|
@@ -881,6 +892,8 @@ export class BlenderRuntimeView {
|
|
|
881
892
|
readonly projection: 'perspective' | 'orthographic';
|
|
882
893
|
readonly lens: number;
|
|
883
894
|
readonly drawMode: 'wireframe' | 'solid' | 'preview' | 'rendered' | null;
|
|
895
|
+
readonly sceneWorld: boolean;
|
|
896
|
+
readonly sceneLights: boolean;
|
|
884
897
|
} | null {
|
|
885
898
|
const saved = this.frame?.view;
|
|
886
899
|
if (!saved) return null;
|
|
@@ -896,6 +909,8 @@ export class BlenderRuntimeView {
|
|
|
896
909
|
projection: saved.perspective === 'ORTHO' ? 'orthographic' : 'perspective',
|
|
897
910
|
lens: saved.lens,
|
|
898
911
|
drawMode: saved.shading ? SAVED_SHADING[saved.shading] : null,
|
|
912
|
+
sceneWorld: saved.scene_world ?? false,
|
|
913
|
+
sceneLights: saved.scene_lights ?? false,
|
|
899
914
|
};
|
|
900
915
|
}
|
|
901
916
|
|
|
@@ -1005,7 +1020,16 @@ export class BlenderRuntimeView {
|
|
|
1005
1020
|
}
|
|
1006
1021
|
|
|
1007
1022
|
stageMode(): string | null {
|
|
1008
|
-
return this.mode;
|
|
1023
|
+
return this.playing ? 'PLAY' : this.mode;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** PLAY IS A MODE OF THE STAGE: while the document plays, the keys are the player's, and the
|
|
1027
|
+
* mode this context publishes is what the frame's key rules stand down on
|
|
1028
|
+
* (`scripts/workbench/generate-keymaps.mjs`, `whenFor`). Blender's own mode returns on stop. */
|
|
1029
|
+
private playing = false;
|
|
1030
|
+
|
|
1031
|
+
setPlaying(playing: boolean): void {
|
|
1032
|
+
this.playing = playing;
|
|
1009
1033
|
}
|
|
1010
1034
|
|
|
1011
1035
|
/** The inverse: the presented object for a Blender datablock NAME, or null
|
|
@@ -1153,15 +1177,26 @@ export class BlenderRuntimeView {
|
|
|
1153
1177
|
* ({@link setRendered}) — the scene's own lights, its World behind and around the model,
|
|
1154
1178
|
* `hide_render` visibility and shadows — seen through `drawCamera()`, the camera the stage
|
|
1155
1179
|
* draws with (orthographic in an orthographic view). `null` returns the viewport to modeling.
|
|
1180
|
+
* Material Preview passes viewport visibility independently of its lighting choices.
|
|
1156
1181
|
* A render taken meanwhile ends back in this state rather than in modeling.
|
|
1157
1182
|
*/
|
|
1158
|
-
|
|
1159
|
-
|
|
1183
|
+
private heldSceneLighting: { world: boolean; lights: boolean } | undefined;
|
|
1184
|
+
holdRendered(drawCamera: (() => THREE.Camera) | null, sceneLighting?: { world: boolean; lights: boolean }, visibility: 'viewport' | 'render' = 'render'): void {
|
|
1185
|
+
if (drawCamera === this.heldRendered && sceneLighting?.world === this.heldSceneLighting?.world && sceneLighting?.lights === this.heldSceneLighting?.lights && visibility === this.heldVisibility) return;
|
|
1160
1186
|
this.heldRendered = drawCamera;
|
|
1187
|
+
this.heldSceneLighting = sceneLighting;
|
|
1188
|
+
this.heldVisibility = visibility;
|
|
1161
1189
|
if (this.capturing) return;
|
|
1162
1190
|
this.report(drawCamera === null ? this.applyRendered(false) : this.applyRendered(true, drawCamera()));
|
|
1163
1191
|
}
|
|
1164
1192
|
|
|
1193
|
+
/** Prepare a rendered viewport before revealing its first frame, using the
|
|
1194
|
+
* same lighting/image/World readiness as a photograph. Call after the stage
|
|
1195
|
+
* has attached the root to its scene. */
|
|
1196
|
+
async prepareRendered(camera: THREE.Camera): Promise<void> {
|
|
1197
|
+
await this.applyRendered(true, camera);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1165
1200
|
/**
|
|
1166
1201
|
* Re-apply the held render lighting if the draw camera or the World changed since it was
|
|
1167
1202
|
* last applied. Cheap when nothing did; the stage calls it every frame it draws.
|
|
@@ -1170,7 +1205,8 @@ export class BlenderRuntimeView {
|
|
|
1170
1205
|
// A photograph in progress keeps its own state; it returns to this one when it ends.
|
|
1171
1206
|
if (this.heldRendered === null || this.capturing) return;
|
|
1172
1207
|
const camera = this.heldRendered();
|
|
1173
|
-
|
|
1208
|
+
const worldKey = this.heldSceneLighting?.world === false ? null : this.worldKeyFor(camera);
|
|
1209
|
+
if (worldKey === this.worldApplied) return;
|
|
1174
1210
|
this.report(this.applyRendered(true, camera));
|
|
1175
1211
|
}
|
|
1176
1212
|
|
|
@@ -1183,7 +1219,12 @@ export class BlenderRuntimeView {
|
|
|
1183
1219
|
const ortho = (camera as THREE.OrthographicCamera).isOrthographicCamera;
|
|
1184
1220
|
// Coarse, so an orbit recomposes a handful of times rather than every frame.
|
|
1185
1221
|
const turn = ortho ? camera.getWorldDirection(new THREE.Vector3()).toArray().map((v) => v.toFixed(1)).join(',') : '';
|
|
1186
|
-
|
|
1222
|
+
// A stage can announce itself before parenting the model into its Scene.
|
|
1223
|
+
// Retry after that attachment; the first World apply otherwise finds no Scene
|
|
1224
|
+
// and its cached key leaves the viewport with no sky or ambient light.
|
|
1225
|
+
let owner: THREE.Object3D = this.root;
|
|
1226
|
+
while (owner.parent) owner = owner.parent;
|
|
1227
|
+
return `${owner.uuid}:${camera.uuid}:${turn}:${this.worldKey}`;
|
|
1187
1228
|
}
|
|
1188
1229
|
|
|
1189
1230
|
private report(work: Promise<void>): void {
|
|
@@ -1258,6 +1299,10 @@ export class BlenderRuntimeView {
|
|
|
1258
1299
|
const start = performance.now();
|
|
1259
1300
|
this.instances.prepareDraw(camera);
|
|
1260
1301
|
this.root.updateMatrixWorld(true);
|
|
1302
|
+
// The script/navigation has just posed the camera and the detached
|
|
1303
|
+
// objects. Fit shadows for that draw, rather than the camera's pose
|
|
1304
|
+
// when Rendered shading was first entered. Captures use this path too.
|
|
1305
|
+
if (this.rendered) this.applyShadows(true, camera);
|
|
1261
1306
|
const opaqueDone = performance.now();
|
|
1262
1307
|
const motion = this.motionGeometry.prepare(camera, !this.rendered && (this.mode ?? this.frame?.mode) === 'OBJECT' &&
|
|
1263
1308
|
options?.interactive === true, options?.height ?? 0);
|
|
@@ -1321,7 +1366,11 @@ export class BlenderRuntimeView {
|
|
|
1321
1366
|
}
|
|
1322
1367
|
|
|
1323
1368
|
private async applyRendered(rendered: boolean, camera?: THREE.Camera): Promise<void> {
|
|
1324
|
-
|
|
1369
|
+
// A frame or shading change can replace geometry, visibility and lights.
|
|
1370
|
+
this.shadowFit = null;
|
|
1371
|
+
const sceneWorld = rendered && (this.capturing || this.heldSceneLighting?.world !== false);
|
|
1372
|
+
const sceneLights = rendered && (this.capturing || this.heldSceneLighting?.lights !== false);
|
|
1373
|
+
const extinction = (sceneWorld ? worldMedium(this.frame?.world)?.extinction : null) ?? new THREE.Vector3();
|
|
1325
1374
|
for (const material of [...this.materials.values(), this.fallback]) applyWorldExtinction(material, extinction);
|
|
1326
1375
|
// An area light cannot be DRAWN until its lookup tables are uploaded, and a
|
|
1327
1376
|
// render is one photograph with no second chance at it.
|
|
@@ -1329,12 +1378,15 @@ export class BlenderRuntimeView {
|
|
|
1329
1378
|
// same way it waits for area-light tables and image decodes: one
|
|
1330
1379
|
// photograph, no second chance at it.
|
|
1331
1380
|
this.rendered = rendered;
|
|
1381
|
+
this.sceneLights = sceneLights;
|
|
1382
|
+
// This is the studio rig, not the authored lights. A scene-lit preview
|
|
1383
|
+
// never adds the Solid studio, including World-only Material Preview.
|
|
1332
1384
|
this.lighting.setRendered(rendered);
|
|
1333
1385
|
// The scene's world is what a render sees past the geometry AND its
|
|
1334
1386
|
// ambient light; modeling keeps the document's own backdrop and fill.
|
|
1335
1387
|
// A photograph always composes its own; the viewport recomposes only when the key changed.
|
|
1336
|
-
const worldKey =
|
|
1337
|
-
if (!
|
|
1388
|
+
const worldKey = sceneWorld && camera ? this.worldKeyFor(camera) : null;
|
|
1389
|
+
if (!sceneWorld) this.world.clear();
|
|
1338
1390
|
else if (this.capturing || worldKey !== this.worldApplied) this.world.apply(this.root, this.frame?.world ?? null, camera);
|
|
1339
1391
|
this.worldApplied = this.capturing ? null : worldKey;
|
|
1340
1392
|
this.applyVisibility();
|
|
@@ -1372,6 +1424,21 @@ export class BlenderRuntimeView {
|
|
|
1372
1424
|
*/
|
|
1373
1425
|
private applyShadows(rendered: boolean, camera?: THREE.Camera): void {
|
|
1374
1426
|
this.root.updateMatrixWorld(true);
|
|
1427
|
+
const fitted = this.shadowFit;
|
|
1428
|
+
if (rendered && fitted && fitted.camera === camera &&
|
|
1429
|
+
(!camera || (fitted.cameraMatrix?.equals(camera.matrixWorld) && fitted.projection?.equals(camera.projectionMatrix))) &&
|
|
1430
|
+
fitted.objects.size === this.objects.size &&
|
|
1431
|
+
[...this.objects.values()].every(object => {
|
|
1432
|
+
const previous = fitted.objects.get(object);
|
|
1433
|
+
return previous?.visible === object.visible && previous.matrix.equals(object.matrixWorld);
|
|
1434
|
+
})) return;
|
|
1435
|
+
this.shadowFit = rendered ? {
|
|
1436
|
+
camera,
|
|
1437
|
+
cameraMatrix: camera?.matrixWorld.clone(),
|
|
1438
|
+
projection: camera?.projectionMatrix.clone(),
|
|
1439
|
+
objects: new Map([...this.objects.values()].map(object =>
|
|
1440
|
+
[object, { matrix: object.matrixWorld.clone(), visible: object.visible }])),
|
|
1441
|
+
} : null;
|
|
1375
1442
|
const boxes: THREE.Box3[] = [];
|
|
1376
1443
|
for (const object of this.objects.values()) {
|
|
1377
1444
|
const mesh = object as THREE.Mesh;
|
|
@@ -1497,12 +1564,14 @@ export class BlenderRuntimeView {
|
|
|
1497
1564
|
}
|
|
1498
1565
|
|
|
1499
1566
|
private applyVisibility(): void {
|
|
1567
|
+
const renderVisibility = this.rendered && (this.capturing || this.heldVisibility === 'render');
|
|
1500
1568
|
for (const obj of this.frame?.objects ?? []) {
|
|
1501
1569
|
const object = this.objects.get(obj.id);
|
|
1502
|
-
|
|
1570
|
+
const visible = renderVisibility ? obj.render_visible : obj.visible;
|
|
1571
|
+
if (object) object.visible = visible;
|
|
1503
1572
|
if (!obj.light) continue;
|
|
1504
1573
|
const light = this.lights.get(obj.light);
|
|
1505
|
-
if (light) light.visible = this.
|
|
1574
|
+
if (light) light.visible = this.sceneLights && visible;
|
|
1506
1575
|
}
|
|
1507
1576
|
// AN OVERLAY IS MODELING CHROME AND IS NEVER PHOTOGRAPHED. The same
|
|
1508
1577
|
// distinction the loop above draws between what the viewport shows and
|
|
@@ -2224,6 +2293,26 @@ export class BlenderRuntimeView {
|
|
|
2224
2293
|
};
|
|
2225
2294
|
}
|
|
2226
2295
|
|
|
2296
|
+
/**
|
|
2297
|
+
* A COPY OF THE MODEL THAT NO LONGER FOLLOWS IT, for Play: the graph `follow` builds, from the
|
|
2298
|
+
* frame this view holds now, and handed nothing afterwards. What moves its objects is the
|
|
2299
|
+
* caller's; Blender's data and this view never see it, and disposing it is the whole undo.
|
|
2300
|
+
*/
|
|
2301
|
+
detach(): { readonly view: BlenderRuntimeView; dispose(): void } {
|
|
2302
|
+
const detached = new BlenderRuntimeView();
|
|
2303
|
+
if (this.frame) detached.applyFrame(this.fullFrame());
|
|
2304
|
+
let disposed = false;
|
|
2305
|
+
return {
|
|
2306
|
+
view: detached,
|
|
2307
|
+
dispose: () => {
|
|
2308
|
+
if (disposed) return;
|
|
2309
|
+
disposed = true;
|
|
2310
|
+
detached.dispose();
|
|
2311
|
+
detached.root.removeFromParent();
|
|
2312
|
+
},
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2227
2316
|
captureSnapshot() {
|
|
2228
2317
|
const frame = this.fullFrame();
|
|
2229
2318
|
const source = this.frame!;
|