@volter/blender-engine 0.1.147 → 0.1.149
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/LICENSE +16 -0
- package/README.md +4 -11
- package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
- package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
- package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
- package/browser/addons/vrm/README.txt +3 -0
- package/browser/addons/vrm/__init__.py +41 -0
- package/browser/addons/vrm/blender_manifest.toml +16 -0
- package/browser/addons/vrm/common/__init__.py +1 -0
- package/browser/addons/vrm/common/animation.py +58 -0
- package/browser/addons/vrm/common/blender_manifest.py +82 -0
- package/browser/addons/vrm/common/char.py +16 -0
- package/browser/addons/vrm/common/convert.py +287 -0
- package/browser/addons/vrm/common/convert_any.py +48 -0
- package/browser/addons/vrm/common/debug.py +40 -0
- package/browser/addons/vrm/common/deep.py +162 -0
- package/browser/addons/vrm/common/error_dialog.py +274 -0
- package/browser/addons/vrm/common/fs.py +71 -0
- package/browser/addons/vrm/common/gl.py +20 -0
- package/browser/addons/vrm/common/gltf.py +1149 -0
- package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
- package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
- package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
- package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
- package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
- package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
- package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
- package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
- package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
- package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
- package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
- package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
- package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
- package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
- package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
- package/browser/addons/vrm/common/legacy_gltf.py +16 -0
- package/browser/addons/vrm/common/logger.py +84 -0
- package/browser/addons/vrm/common/mtoon0.blend +0 -0
- package/browser/addons/vrm/common/mtoon1.blend +0 -0
- package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
- package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
- package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
- package/browser/addons/vrm/common/ops/__init__.py +10 -0
- package/browser/addons/vrm/common/ops/export_scene.py +67 -0
- package/browser/addons/vrm/common/ops/icyp.py +53 -0
- package/browser/addons/vrm/common/ops/import_scene.py +57 -0
- package/browser/addons/vrm/common/ops/vrm.py +2380 -0
- package/browser/addons/vrm/common/ops/wm.py +167 -0
- package/browser/addons/vrm/common/preferences.py +362 -0
- package/browser/addons/vrm/common/progress.py +87 -0
- package/browser/addons/vrm/common/rotation.py +89 -0
- package/browser/addons/vrm/common/safe_removal.py +46 -0
- package/browser/addons/vrm/common/scene_watcher.py +230 -0
- package/browser/addons/vrm/common/shader.py +2243 -0
- package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
- package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
- package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
- package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
- package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
- package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
- package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
- package/browser/addons/vrm/common/version.py +161 -0
- package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
- package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
- package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
- package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
- package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
- package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
- package/browser/addons/vrm/common/workspace.py +227 -0
- package/browser/addons/vrm/common/writable_context.py +77 -0
- package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
- package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
- package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
- package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
- package/browser/addons/vrm/documentation/en-us/index.md +41 -0
- package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
- package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
- package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
- package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
- package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
- package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
- package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
- package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
- package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
- package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
- package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
- package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
- package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
- package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
- package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
- package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
- package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
- package/browser/addons/vrm/editor/__init__.py +1 -0
- package/browser/addons/vrm/editor/extension.py +425 -0
- package/browser/addons/vrm/editor/extension_accessor.py +75 -0
- package/browser/addons/vrm/editor/handler.py +84 -0
- package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
- package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
- package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
- package/browser/addons/vrm/editor/make_armature.py +709 -0
- package/browser/addons/vrm/editor/menu.py +147 -0
- package/browser/addons/vrm/editor/migration.py +297 -0
- package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
- package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
- package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
- package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
- package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
- package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
- package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
- package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
- package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
- package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
- package/browser/addons/vrm/editor/ops.py +471 -0
- package/browser/addons/vrm/editor/panel.py +315 -0
- package/browser/addons/vrm/editor/property_group.py +888 -0
- package/browser/addons/vrm/editor/search.py +682 -0
- package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
- package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
- package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
- package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
- package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
- package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
- package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
- package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
- package/browser/addons/vrm/editor/subscription.py +123 -0
- package/browser/addons/vrm/editor/t_pose.py +713 -0
- package/browser/addons/vrm/editor/validation.py +1271 -0
- package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
- package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
- package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
- package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
- package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
- package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
- package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
- package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
- package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
- package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
- package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
- package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
- package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
- package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
- package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
- package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
- package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
- package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
- package/browser/addons/vrm/exporter/__init__.py +1 -0
- package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
- package/browser/addons/vrm/exporter/export_scene.py +930 -0
- package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
- package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
- package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
- package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
- package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
- package/browser/addons/vrm/external/__init__.py +1 -0
- package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
- package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
- package/browser/addons/vrm/external/rigify_support.py +1 -0
- package/browser/addons/vrm/importer/__init__.py +1 -0
- package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
- package/browser/addons/vrm/importer/file_handler.py +34 -0
- package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
- package/browser/addons/vrm/importer/import_scene.py +646 -0
- package/browser/addons/vrm/importer/license_validation.py +198 -0
- package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
- package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
- package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
- package/browser/addons/vrm/importer/vrm_diff.py +316 -0
- package/browser/addons/vrm/locale/__init__.py +1 -0
- package/browser/addons/vrm/locale/ja_jp.py +826 -0
- package/browser/addons/vrm/locale/ruff.toml +8 -0
- package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
- package/browser/addons/vrm/locale/zh_hans.py +928 -0
- package/browser/addons/vrm/registration.py +670 -0
- package/browser/addons/vrm.json +18 -0
- package/browser/blender-emscripten-engine.mts +60 -128
- package/browser/blender-engine.mts +20 -186
- package/browser/blender-wali-engine.mts +35 -81
- package/browser/describe-thrown.mts +3 -6
- package/browser/document-chunks.mts +4 -16
- package/browser/gltf_export.py +3161 -0
- package/browser/heap-growth.mts +23 -0
- package/browser/index.ts +2 -0
- package/browser/protocol.ts +119 -35
- package/browser/pull-job.mts +2 -10
- package/browser/rna.ts +190 -73
- package/browser/runtime.ts +197 -82
- package/browser/session-addons.mts +33 -0
- package/browser/session-frame.mts +5 -24
- package/browser/session.py +1338 -1427
- package/browser/three/attach-presenter.ts +11 -39
- package/browser/three/blender-base64.ts +1 -36
- package/browser/three/blender-corner-normals.ts +1 -23
- package/browser/three/blender-display-shaders.json +1 -1
- package/browser/three/blender-display-shaders.lut +0 -0
- package/browser/three/blender-display-transform.ts +2 -1
- package/browser/three/blender-frame-columns.ts +5 -13
- package/browser/three/blender-graph-material.ts +18 -3
- package/browser/three/blender-node-graph.ts +10 -34
- package/browser/three/blender-physical-material.ts +2 -31
- package/browser/three/blender-runtime-armature.ts +7 -60
- package/browser/three/blender-runtime-frame.ts +9 -23
- package/browser/three/blender-runtime-geometry.ts +22 -26
- package/browser/three/blender-runtime-lighting.ts +48 -178
- package/browser/three/blender-runtime-shadows.ts +187 -1
- package/browser/three/blender-runtime-skeleton.ts +53 -1
- package/browser/three/blender-runtime-view.ts +505 -283
- package/browser/three/blender-runtime-weights.ts +9 -72
- package/browser/three/blender-sky.ts +13 -33
- package/browser/three/blender-triangulate.ts +7 -35
- package/browser/three/blender-viewport-sampling.ts +235 -0
- package/browser/three/blender-workbench-material.ts +25 -29
- package/browser/three/blender-workbench-outline.ts +199 -0
- package/browser/three/blender-world-volume.ts +62 -10
- package/browser/three/presenter.ts +10 -26
- package/browser/three/release.ts +5 -21
- package/browser/three/sky-precompute-worker.ts +1 -18
- package/browser/three/sky-worker.ts +5 -23
- package/browser/three/world-field-sampler.ts +12 -26
- package/browser/volter_three.py +9 -105
- package/browser/worker.ts +156 -151
- package/package.json +5 -2
- package/wasm/BUNDLE.json +17 -17
- package/wasm/DEPENDENCY-LICENSES.txt +14 -0
- package/wasm/blender_browser.data.br +0 -0
- package/wasm/blender_browser.js +1 -1
- package/wasm/blender_browser.wasm.br +0 -0
- package/browser/frame-transfer.mts +0 -19
- package/browser/three/agx-base-srgb.lut +0 -0
- package/browser/three/agx-look-medium-high-contrast.lut +0 -0
- package/browser/three/agx-look-punchy.lut +0 -0
- package/browser/three/blender-agx.ts +0 -235
- package/browser/three/blender-display-lut.ts +0 -145
- package/browser/three/blender-filmic.ts +0 -49
- package/browser/three/blender-standard.ts +0 -68
- package/browser/three/blender-world-visibility.ts +0 -278
- package/browser/three/filmic-srgb.lut +0 -0
|
@@ -26,11 +26,11 @@ import {
|
|
|
26
26
|
WorldBackground,
|
|
27
27
|
worldSchema,
|
|
28
28
|
} from './blender-runtime-lighting';
|
|
29
|
-
import { fitModelDirectionalShadow, visibleShadowReceivers } from './blender-runtime-shadows';
|
|
30
|
-
import {BlenderWorldVisibility,WORLD_VISIBILITY,bindWorldVisibility} from './blender-world-visibility';
|
|
29
|
+
import { fitModelDirectionalShadow, farthestVisibleReceiver, fitPerspectiveShadowBias, VisibleReceivers, visibleShadowReceivers } from './blender-runtime-shadows';
|
|
31
30
|
import { volumeMesh, volumeSchema } from './blender-runtime-volume';
|
|
32
31
|
import { applyPhysicalMaterial, applyWorldExtinction, physicalMaterialSchema } from './blender-physical-material';
|
|
33
|
-
import { prepareGraphGeometry, setMaterialGraph } from './blender-graph-material';
|
|
32
|
+
import { photographGraphs, prepareGraphGeometry, setMaterialGraph } from './blender-graph-material';
|
|
33
|
+
import { type BlenderPhotographSampling, eeveeSampling, workbenchSampling } from './blender-viewport-sampling';
|
|
34
34
|
import {BlenderObjectInfoMaterials} from './blender-object-info-materials';
|
|
35
35
|
import { type CompiledGraph, compileMaterialGraph, graphSeeThrough, materialGraphSchema } from './blender-node-graph';
|
|
36
36
|
import {worldMedium, WorldVolumePass} from './blender-world-volume';
|
|
@@ -46,11 +46,32 @@ import {
|
|
|
46
46
|
panCameraView,
|
|
47
47
|
} from './blender-runtime-camera-view';
|
|
48
48
|
import { DEFAULT_VIEWPORT_DISPLAY, workbenchMaterial } from './blender-workbench-material';
|
|
49
|
+
import { WorkbenchOutline } from './blender-workbench-outline';
|
|
50
|
+
import type { BlenderDisplaySettings } from './blender-display-transform';
|
|
49
51
|
import { BlenderRuntimeInstances } from './blender-runtime-instances';
|
|
50
52
|
import { BlenderTransparentInstances } from './blender-transparent-instances';
|
|
51
53
|
import { BlenderMaterialRanges } from './blender-material-ranges';
|
|
52
54
|
import { BlenderMotionGeometry } from './blender-motion-geometry';
|
|
53
55
|
|
|
56
|
+
/** What the stage tells {@link BlenderRuntimeView.prepareDraw} about the draw it is about to make. */
|
|
57
|
+
export interface BlenderDrawOptions {
|
|
58
|
+
readonly interactive: boolean;
|
|
59
|
+
readonly height: number;
|
|
60
|
+
readonly multiDraw?: boolean;
|
|
61
|
+
readonly renderer?: THREE.WebGLRenderer;
|
|
62
|
+
/** The stage's draw mode (`solid`, `rendered`, ...): Solid's object outline is Solid's alone. */
|
|
63
|
+
readonly drawMode?: string;
|
|
64
|
+
/** Target pixels per output pixel of this draw (Blender's `ui_scale`): the device pixel ratio
|
|
65
|
+
* on screen, the supersampling factor in a photograph. */
|
|
66
|
+
readonly pixelScale?: number;
|
|
67
|
+
/** This draw is a photograph: it gets one chance, so nothing it shows may settle later. */
|
|
68
|
+
readonly photograph?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The Preferences' Viewport Anti-Aliasing (`U.viewport_aa`) at its factory value: the tab holds
|
|
72
|
+
* no Blender preferences. */
|
|
73
|
+
const PREFERENCES_VIEWPORT_AA = 8;
|
|
74
|
+
|
|
54
75
|
/** A saved `View3DShading.type` as the stage's draw mode: Material Preview is `preview`. */
|
|
55
76
|
const SAVED_SHADING = {
|
|
56
77
|
WIREFRAME: 'wireframe',
|
|
@@ -148,6 +169,32 @@ const drawnSkinSchema = z
|
|
|
148
169
|
weight: z.instanceof(Float32Array),
|
|
149
170
|
})
|
|
150
171
|
.strict();
|
|
172
|
+
/**
|
|
173
|
+
* ONE MESH THE PRESENTER CANNOT DRAW IS SKIPPED BY NAME; the rest of the frame presents.
|
|
174
|
+
*
|
|
175
|
+
* A throw anywhere in a frame would refuse the WHOLE frame: the Model document would keep showing
|
|
176
|
+
* the state before the call, every later present would fail the same way, and one mesh (a custom
|
|
177
|
+
* vertex attribute named as its render UV, say) would hide thousands of other objects.
|
|
178
|
+
*
|
|
179
|
+
* The skipped mesh is held as an empty geometry AT ITS SIGNATURE, so the session's record (it
|
|
180
|
+
* sent the mesh at that revision) and this presenter agree, and the next frame's reference to it
|
|
181
|
+
* is not a resync. Its objects stand in the graph with nothing to draw. It is drawn again when its
|
|
182
|
+
* revision changes, which is the edit that may have fixed it.
|
|
183
|
+
*/
|
|
184
|
+
export const REFUSED_GEOMETRY = 'blenderRefused';
|
|
185
|
+
function drawnOrRefused(draw: () => THREE.BufferGeometry): THREE.BufferGeometry {
|
|
186
|
+
try {
|
|
187
|
+
return draw();
|
|
188
|
+
} catch (error) {
|
|
189
|
+
const geometry = new THREE.BufferGeometry();
|
|
190
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(0), 3));
|
|
191
|
+
geometry.userData[REFUSED_GEOMETRY] = error instanceof Error ? error.message : String(error);
|
|
192
|
+
return geometry;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Each (mesh, revision, reason) is named once per tab, not once per present or per follower. */
|
|
196
|
+
const NAMED_REFUSALS = new Set<string>();
|
|
197
|
+
|
|
151
198
|
/** An exported mesh's geometry with its skin laid over its drawn vertices (`skinGeometry`). */
|
|
152
199
|
function withFrameSkin(geometry: THREE.BufferGeometry, skin: FrameSkin | undefined): THREE.BufferGeometry {
|
|
153
200
|
if (skin) skinGeometry(geometry, skin);
|
|
@@ -218,10 +265,8 @@ const unchangedMeshSchema = z
|
|
|
218
265
|
/**
|
|
219
266
|
* BLENDER'S OWN ARRAYS, as the columns the draw reads.
|
|
220
267
|
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* draws it, so the presenter takes the columns rather than a drawn mesh. The
|
|
224
|
-
* buffers arrive transferred from the worker (`session-frame.mts` reads them
|
|
268
|
+
* The native export door emits these columns; `drawArraysFromColumns`
|
|
269
|
+
* builds their drawn geometry. Buffers arrive transferred from the worker (`session-frame.mts` reads them
|
|
225
270
|
* out of the module filesystem), which is why every one is a typed array and
|
|
226
271
|
* none is an array of numbers.
|
|
227
272
|
*/
|
|
@@ -283,50 +328,24 @@ const meshSchema = z.union([
|
|
|
283
328
|
meshJsonSchema,
|
|
284
329
|
unchangedMeshSchema,
|
|
285
330
|
]);
|
|
286
|
-
/** Texture decodes still in flight. A RENDER MUST NOT START BEFORE THEY LAND:
|
|
287
|
-
* `createImageBitmap` is asynchronous, so a frame drawn in the same turn the
|
|
288
|
-
* material arrived would use an empty texture and look exactly like the
|
|
289
|
-
* untextured render this whole path exists to stop. (A RASTER needs no wait:
|
|
290
|
-
* a `DataTexture` holds its bytes the moment it is built.) */
|
|
291
|
-
const pendingTextures = new Set<Promise<void>>();
|
|
292
|
-
|
|
293
|
-
export async function texturesReady(): Promise<void> {
|
|
294
|
-
while (pendingTextures.size > 0) await Promise.all([...pendingTextures]);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
331
|
/** An image file's own bytes (PNG or JPEG) as a three texture, decoded by the browser's own
|
|
298
332
|
* decoder. */
|
|
299
333
|
function loadEncodedTexture(
|
|
300
334
|
encoded: Uint8Array,
|
|
301
|
-
mime: 'image/png' | 'image/jpeg',
|
|
335
|
+
mime: 'image/png' | 'image/jpeg' | 'image/webp',
|
|
302
336
|
): { texture: THREE.Texture; ready: Promise<void> } {
|
|
303
337
|
const blob = new Blob([encoded as BlobPart], { type: mime });
|
|
304
338
|
const texture = new THREE.Texture();
|
|
339
|
+
// Decoded rows are bottom-first for Blender's v-up UVs. Keep flipY false
|
|
340
|
+
// for exporters as well; WebGL ignores that flag for ImageBitmap sources.
|
|
341
|
+
texture.flipY = false;
|
|
305
342
|
let disposed = false;
|
|
306
343
|
texture.addEventListener('dispose', () => {
|
|
307
344
|
disposed = true;
|
|
308
|
-
|
|
345
|
+
// r186's `Texture` types its image by a parameter, unknown here: an ImageBitmap once decoded.
|
|
346
|
+
(texture.image as ImageBitmap | null | undefined)?.close?.();
|
|
309
347
|
});
|
|
310
|
-
//
|
|
311
|
-
// stores its rows top-down -- this one is a FILE-backed image's own bytes,
|
|
312
|
-
// sent as they sit on disk -- while a UV's v=0 in Blender samples the
|
|
313
|
-
// image's BOTTOM row. A bitmap decoded in the PNG's own order therefore drew
|
|
314
|
-
// every textured surface upside down: measured on `20-rigged-courier`, the
|
|
315
|
-
// courier's painted face came back with the brows below the eyes and the
|
|
316
|
-
// mouth under the hat.
|
|
317
|
-
//
|
|
318
|
-
// The flip has to be asked for HERE and cannot be asked for on the texture:
|
|
319
|
-
// `Texture.flipY` (and `premultiplyAlpha`) are `UNPACK_FLIP_Y_WEBGL` state,
|
|
320
|
-
// which WebGL ignores for an ImageBitmap source -- three says so itself at
|
|
321
|
-
// `three/src/textures/Texture.js:274` (0.180.0), "this property has no
|
|
322
|
-
// effect when using `ImageBitmap`. You need to configure the flip on bitmap
|
|
323
|
-
// creation instead."
|
|
324
|
-
//
|
|
325
|
-
// Its alpha is asked for here too: the browser's `default` premultiplies (Chrome's does), and
|
|
326
|
-
// every reader of this texture takes the file's own texels, straight unless the image says it
|
|
327
|
-
// stores them premultiplied, which the reader then undoes. And its colour is the file's own: no
|
|
328
|
-
// embedded profile or gamma is applied, because Blender reads the texels as stored and applies
|
|
329
|
-
// the image's colour space setting itself, as the sampler does here.
|
|
348
|
+
// Decode image bitmaps bottom-row-first for Blender UVs, with explicit alpha and colour-conversion options. WebGL ignores Texture.flipY and premultiplyAlpha for ImageBitmap sources.
|
|
330
349
|
const decoding = createImageBitmap(blob, {
|
|
331
350
|
imageOrientation: 'flipY',
|
|
332
351
|
premultiplyAlpha: 'none',
|
|
@@ -340,11 +359,7 @@ function loadEncodedTexture(
|
|
|
340
359
|
texture.image = bitmap;
|
|
341
360
|
texture.needsUpdate = true;
|
|
342
361
|
presenterChanged();
|
|
343
|
-
})
|
|
344
|
-
.finally(() => {
|
|
345
|
-
pendingTextures.delete(decoding);
|
|
346
362
|
});
|
|
347
|
-
pendingTextures.add(decoding);
|
|
348
363
|
// Keep a rejection handled even if a document closes before its first
|
|
349
364
|
// render. The owner's ready promise still reports that same failure.
|
|
350
365
|
void decoding.catch(() => {});
|
|
@@ -425,8 +440,8 @@ function udimTextures(
|
|
|
425
440
|
* one; a FILE's own PNG does not, because the decoder reads it off the bitmap
|
|
426
441
|
* and asking Blender would have decoded the file just to answer
|
|
427
442
|
* (`_image_values.frame_image`). Both carry the REVISION those bytes are. The
|
|
428
|
-
* `*Base64` spellings are the same bytes on a channel with no transfer list
|
|
429
|
-
|
|
443
|
+
* `*Base64` spellings are the same bytes on a channel with no transfer list.
|
|
444
|
+
*/
|
|
430
445
|
const rasterImageSchema = z
|
|
431
446
|
.object({
|
|
432
447
|
width: z.number().int().positive(),
|
|
@@ -447,16 +462,13 @@ const rasterImageSchema = z
|
|
|
447
462
|
}).strict()).optional(),
|
|
448
463
|
})
|
|
449
464
|
.strict();
|
|
450
|
-
/** An
|
|
451
|
-
* already carries compressed, decoded by the browser into the texture, so neither side holds
|
|
452
|
-
* the picture as raw RGBA. MEASURED 2026-09-29 on the Stoneguard bridge: its 84 material images
|
|
453
|
-
* are 171 MB as their packed files and 1,343 MB as RGBA. */
|
|
465
|
+
/** An encoded PNG or JPEG from session.py::_encoded_image, decoded by the browser without carrying raw RGBA through the frame. */
|
|
454
466
|
const encodedImageSchema = z
|
|
455
467
|
.object({
|
|
456
468
|
revision: z.number().int().nonnegative(),
|
|
457
469
|
encoded: z.instanceof(Uint8Array).optional(),
|
|
458
470
|
encodedBase64: z.string().optional(),
|
|
459
|
-
mime: z.enum(['image/png', 'image/jpeg']),
|
|
471
|
+
mime: z.enum(['image/png', 'image/jpeg', 'image/webp']),
|
|
460
472
|
colorspace: z.enum(['sRGB', 'data']).optional(),
|
|
461
473
|
})
|
|
462
474
|
.strict();
|
|
@@ -510,6 +522,12 @@ const materialSchema = z
|
|
|
510
522
|
.object({ color: z.tuple([scalar, scalar, scalar, scalar]), roughness: scalar, metallic: scalar })
|
|
511
523
|
.strict()
|
|
512
524
|
.optional(),
|
|
525
|
+
/** `Material.use_backface_culling_shadow`: shadows from the faces turned to the light only. */
|
|
526
|
+
backface_culling_shadow: z.boolean().optional(),
|
|
527
|
+
/** `Material.use_backface_culling`: the back faces are not drawn (Rendered and Material Preview). */
|
|
528
|
+
backface_culling: z.boolean().optional(),
|
|
529
|
+
/** `Material.use_transparency_overlap`: a blended surface draws all its faces in one pass. */
|
|
530
|
+
transparency_overlap: z.boolean().optional(),
|
|
513
531
|
})
|
|
514
532
|
.strict();
|
|
515
533
|
/** THE FRAME CONTRACT, and the reason it is exported: the C++ export door
|
|
@@ -595,8 +613,7 @@ export const frameSchema = z
|
|
|
595
613
|
/** `scene.frame_current`. */
|
|
596
614
|
frame: z.number().int().default(0),
|
|
597
615
|
/**
|
|
598
|
-
*
|
|
599
|
-
* "Inspection parity", I4), keyed by armature OBJECT name. What Blender's
|
|
616
|
+
* Inspection overlays keyed by armature object name. What Blender's
|
|
600
617
|
* own overlay engine READS, never what it draws: per bone a pose matrix, a
|
|
601
618
|
* length, a parent and its flags, plus the armature's display type and
|
|
602
619
|
* `show_in_front`. `blender-runtime-armature.ts` is the drawing.
|
|
@@ -639,9 +656,44 @@ export const frameSchema = z
|
|
|
639
656
|
shading: z.enum(['WIREFRAME', 'SOLID', 'MATERIAL', 'RENDERED']).optional(),
|
|
640
657
|
scene_world: z.boolean().optional(),
|
|
641
658
|
scene_lights: z.boolean().optional(),
|
|
659
|
+
/** Solid's object outline (`View3DShading.show_object_outline`) and its colour. */
|
|
660
|
+
show_object_outline: z.boolean().optional(),
|
|
661
|
+
object_outline_color: z.tuple([z.number(), z.number(), z.number()]).optional(),
|
|
662
|
+
/** Solid's Specular Lighting (`View3DShading.show_specular_highlight`). */
|
|
663
|
+
show_specular_highlight: z.boolean().optional(),
|
|
664
|
+
/** What Solid and Wireframe draw behind the model (`View3DShading.background_type`), the
|
|
665
|
+
* VIEWPORT colour (`background_color`, scene-linear) and the World's Viewport Display
|
|
666
|
+
* colour (`World.color`, scene-linear) a WORLD background shows. */
|
|
667
|
+
background_type: z.enum(['THEME', 'WORLD', 'VIEWPORT']).optional(),
|
|
668
|
+
background_color: z.tuple([z.number(), z.number(), z.number()]).optional(),
|
|
669
|
+
world_color: z.tuple([z.number(), z.number(), z.number()]).nullable().optional(),
|
|
642
670
|
})
|
|
643
671
|
.nullable()
|
|
644
672
|
.optional(),
|
|
673
|
+
/** How the viewport anti-aliases (`session.py`): the scene's engine, Workbench's
|
|
674
|
+
* `display.viewport_aa`, EEVEE's viewport `taa_samples` and the film's `filter_size`. */
|
|
675
|
+
sampling: z
|
|
676
|
+
.object({
|
|
677
|
+
engine: z.string(),
|
|
678
|
+
viewport_aa: z.string(),
|
|
679
|
+
taa_samples: z.number().int().nonnegative(),
|
|
680
|
+
filter_size: z.number().finite().nonnegative(),
|
|
681
|
+
})
|
|
682
|
+
.strict()
|
|
683
|
+
.optional(),
|
|
684
|
+
/** The scene's colour management as this frame was presented with it (`session.py`):
|
|
685
|
+
* `view_settings` (exposure in stops) and the display device. Rendered and Material Preview
|
|
686
|
+
* are drawn through it ({@link BlenderRuntimeView.displaySettings}). */
|
|
687
|
+
display: z
|
|
688
|
+
.object({
|
|
689
|
+
view_transform: z.string(),
|
|
690
|
+
look: z.string(),
|
|
691
|
+
exposure: z.number().finite(),
|
|
692
|
+
gamma: z.number().finite(),
|
|
693
|
+
display_device: z.string(),
|
|
694
|
+
})
|
|
695
|
+
.strict()
|
|
696
|
+
.optional(),
|
|
645
697
|
/** The viewport's subject line as Blender composes it (`draw_selected_name`), less the
|
|
646
698
|
* frame and its marker, with the scene's markers in list order. */
|
|
647
699
|
subject: z
|
|
@@ -657,7 +709,8 @@ export const frameSchema = z
|
|
|
657
709
|
.optional(),
|
|
658
710
|
})
|
|
659
711
|
.strict();
|
|
660
|
-
|
|
712
|
+
/** One presented frame, as `applyFrame` takes it (`frameSchema`). */
|
|
713
|
+
export type Frame = z.infer<typeof frameSchema>;
|
|
661
714
|
|
|
662
715
|
/** `BKE_scene_find_marker_name`: the list walked from both ends at once, the front one asked
|
|
663
716
|
* first, so of several markers on one frame the answer is not simply the first. */
|
|
@@ -693,6 +746,32 @@ const LENGTH_UNITS: Readonly<Record<'METRIC' | 'IMPERIAL', readonly (readonly [n
|
|
|
693
746
|
[1609.344, 'Miles'],
|
|
694
747
|
],
|
|
695
748
|
};
|
|
749
|
+
/** The factory theme's 3D Viewport background (`userdef_default_theme.c`, `high_gradient`,
|
|
750
|
+
* single colour): 0x3d. */
|
|
751
|
+
const THEME_VIEWPORT_BACK = 0x3d / 255;
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* SOLID'S BACKGROUND as Blender's viewport shows it (`overlay_background.cc`), for a saved 3D
|
|
755
|
+
* View. The overlay draws it "alpha under" the render, and the OCIO display shader merges the two
|
|
756
|
+
* in display-linear: `pow(c, 2.2)`, the overlay added, `pow(c, 1/2.2)`
|
|
757
|
+
* (`gpu_shader_display_transform_frag.glsl`). So a colour the overlay holds is shown as
|
|
758
|
+
* `pow(c, 1/2.2)`:
|
|
759
|
+
* - THEME: the theme's colour taken to linear by `srgb_to_linearrgb` (`overlay_instance.cc`), so
|
|
760
|
+
* the factory grey 0x3d (61) is shown as 63;
|
|
761
|
+
* - WORLD: the World's Viewport Display colour (`world->horr`), as it is;
|
|
762
|
+
* - VIEWPORT: the shading's `background_color`, as it is.
|
|
763
|
+
* The stage photographs Solid over this colour by the same merge (`PhotographSampling.backdropMerge`).
|
|
764
|
+
*/
|
|
765
|
+
function savedBackground(saved: NonNullable<Frame['view']>): string {
|
|
766
|
+
const theme = THREE.MathUtils.clamp(THEME_VIEWPORT_BACK, 0, 1);
|
|
767
|
+
const themeLinear = new THREE.Color().setRGB(theme, theme, theme, THREE.SRGBColorSpace);
|
|
768
|
+
const held = saved.background_type === 'WORLD' ? (saved.world_color ?? null)
|
|
769
|
+
: saved.background_type === 'VIEWPORT' ? (saved.background_color ?? null) : null;
|
|
770
|
+
const [r, g, b] = held ?? [themeLinear.r, themeLinear.g, themeLinear.b];
|
|
771
|
+
const shown = (c: number) => Math.round(Math.max(0, Math.min(1, c)) ** (1 / 2.2) * 255);
|
|
772
|
+
return `#${[r, g, b].map((c) => shown(c).toString(16).padStart(2, '0')).join('')}`;
|
|
773
|
+
}
|
|
774
|
+
|
|
696
775
|
/** One render's two poses, in Blender's own (Z-up) frame. */
|
|
697
776
|
export interface PhotographRecord {
|
|
698
777
|
/** `scene.camera.matrix_world`, as `session.py::_photograph` sent it. */
|
|
@@ -742,31 +821,26 @@ export class BlenderRuntimeView {
|
|
|
742
821
|
private readonly lights = new Map<string, THREE.Light>();
|
|
743
822
|
private readonly lighting = new ViewportLighting();
|
|
744
823
|
private readonly world = new WorldBackground();
|
|
745
|
-
private readonly worldVisibility=new BlenderWorldVisibility();
|
|
746
824
|
/**
|
|
747
|
-
*
|
|
825
|
+
* Inspection overlays are not children of {@link root}, and
|
|
748
826
|
* that is the whole design: an overlay is EDITOR FURNITURE, so its
|
|
749
827
|
* visibility belongs to the viewport's Helpers menu rather than to this
|
|
750
828
|
* presenter, and the door for that is the host's own — a document hands
|
|
751
|
-
* each group to
|
|
752
|
-
* (`@volter/sdk/host`, `EditorHostStage`), which marks it
|
|
829
|
+
* each group to editor-threejs's viewport `setHelper(kind, object)`, which marks it
|
|
753
830
|
* `editorHelper`, keeps it out of the hierarchy and the raycast, and turns
|
|
754
831
|
* it on and off with the kind's checkbox. `blender-runtime.document.tsx`
|
|
755
832
|
* is where they are handed over.
|
|
756
833
|
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
* not inherit this root's matrix.
|
|
834
|
+
* Each helper root carries the Blender → document permutation because it
|
|
835
|
+
* is a scene-root object and does not inherit this root's matrix.
|
|
760
836
|
*/
|
|
761
837
|
private readonly armatureOverlay = new ArmatureOverlay();
|
|
762
838
|
private readonly weightOverlay = new WeightOverlay();
|
|
763
839
|
private readonly cursorOverlay = new CursorOverlay();
|
|
764
840
|
/** Blender's overlay extras: cameras, lights and empties (`blender-runtime-extras.ts`). */
|
|
765
841
|
private readonly extrasOverlay = new ExtrasOverlay();
|
|
766
|
-
/** The
|
|
767
|
-
*
|
|
768
|
-
* is modeling chrome and never appears in a photograph — the same
|
|
769
|
-
* distinction `applyVisibility` draws for the scene's own objects). */
|
|
842
|
+
/** The Helpers menu controls these roots' visibility. Their inner overlay
|
|
843
|
+
* groups are hidden during photographs without changing the menu state. */
|
|
770
844
|
private readonly armatureRoot = new THREE.Group();
|
|
771
845
|
private readonly weightRoot = new THREE.Group();
|
|
772
846
|
private readonly cursorRoot = new THREE.Group();
|
|
@@ -778,6 +852,8 @@ export class BlenderRuntimeView {
|
|
|
778
852
|
private readonly hiddenSurfaceLayers = new WeakMap<THREE.Object3D,number>();
|
|
779
853
|
private shadowFit: {
|
|
780
854
|
camera: THREE.Camera | undefined;
|
|
855
|
+
/** Fitted to the surfaces the camera sees (a renderer was at hand), not to the frustum. */
|
|
856
|
+
visible: boolean;
|
|
781
857
|
cameraMatrix: THREE.Matrix4 | undefined;
|
|
782
858
|
projection: THREE.Matrix4 | undefined;
|
|
783
859
|
solarLights: THREE.DirectionalLight[];
|
|
@@ -799,7 +875,11 @@ export class BlenderRuntimeView {
|
|
|
799
875
|
* (`blender-workbench-material.ts`) over their viewport display. */
|
|
800
876
|
private workbench = false;
|
|
801
877
|
private readonly workbenchMaterials = new Map<string, THREE.Material>();
|
|
802
|
-
|
|
878
|
+
/** Solid's object outline (`blender-workbench-outline.ts`). */
|
|
879
|
+
private readonly outline = new WorkbenchOutline();
|
|
880
|
+
/** What the drawing camera sees, for a sun's shadow map (`blender-runtime-shadows.ts`). */
|
|
881
|
+
private readonly visibleReceivers = new VisibleReceivers();
|
|
882
|
+
private readonly fallback = new THREE.MeshPhysicalMaterial({ color: 0xb9bec6, roughness: 0.72, side: THREE.DoubleSide, shadowSide: THREE.DoubleSide });
|
|
803
883
|
/** Base Color images, by image name -- ONE texture per image however many
|
|
804
884
|
* materials read it, and the cache OWNS it: a material points at one and
|
|
805
885
|
* never disposes it. Held with the size and revision the resident bytes
|
|
@@ -812,7 +892,7 @@ export class BlenderRuntimeView {
|
|
|
812
892
|
width: number;
|
|
813
893
|
height: number;
|
|
814
894
|
revision: number;
|
|
815
|
-
encoded?: { bytes: Uint8Array; mime: 'image/png' | 'image/jpeg'; colorspace: 'sRGB' | 'data' };
|
|
895
|
+
encoded?: { bytes: Uint8Array; mime: 'image/png' | 'image/jpeg' | 'image/webp'; colorspace: 'sRGB' | 'data' };
|
|
816
896
|
ready?: Promise<void>;
|
|
817
897
|
/** A UDIM image's tiles as a graph samples them (`udimTextures`). */
|
|
818
898
|
udim?: {tiles: THREE.DataArrayTexture; map: THREE.DataTexture; frame: NonNullable<z.infer<typeof rasterImageSchema>['tiles']>};
|
|
@@ -856,12 +936,11 @@ export class BlenderRuntimeView {
|
|
|
856
936
|
private readonly submittedMeshes = new Map<string, unknown>();
|
|
857
937
|
private submitted: Record<string, unknown> | null = null;
|
|
858
938
|
private submittedSession: string | null = null;
|
|
859
|
-
/**
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
|
|
864
|
-
* leaves both poses behind instead of only an exception. */
|
|
939
|
+
/**
|
|
940
|
+
* Record the scene-camera pose sent by Blender and the photograph’s actual pose transformed back
|
|
941
|
+
* into Blender’s frame. Python asserts equality on every render; retain both poses before that
|
|
942
|
+
* assertion can throw.
|
|
943
|
+
*/
|
|
865
944
|
private readonly photographs: PhotographRecord[] = [];
|
|
866
945
|
/** WHO WANTS TO KNOW THE MODEL MOVED. The Properties sections read the
|
|
867
946
|
* engine through the RNA door, and a frame is the one signal in the tab
|
|
@@ -876,14 +955,12 @@ export class BlenderRuntimeView {
|
|
|
876
955
|
private leader: BlenderRuntimeView | null = null;
|
|
877
956
|
|
|
878
957
|
/**
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
* other area — a Rendered one, as Blender's always-on render preview is — stood still until the
|
|
883
|
-
* release, where Blender redraws every area on every move. Each view holds its own object for
|
|
884
|
-
* the same Blender object (the frame's id) under the same parent, so the local matrix carries.
|
|
958
|
+
* Mirror a live gizmo move into every other view of the same model using the presented object’s
|
|
959
|
+
* local matrix. Blender confirms the pose when the gesture ends. Signal every move so drawing and
|
|
960
|
+
* temporal accumulation follow it in single-view and split-view documents.
|
|
885
961
|
*/
|
|
886
962
|
mirrorPose(object: THREE.Object3D): void {
|
|
963
|
+
presenterChanged();
|
|
887
964
|
let id: string | null = null;
|
|
888
965
|
for (const [key, held] of this.objects)
|
|
889
966
|
if (held === object) {
|
|
@@ -892,7 +969,6 @@ export class BlenderRuntimeView {
|
|
|
892
969
|
}
|
|
893
970
|
if (id === null) return;
|
|
894
971
|
const views = this.leader ? [this.leader, ...this.leader.followers] : [...this.followers];
|
|
895
|
-
let moved = false;
|
|
896
972
|
for (const view of views) {
|
|
897
973
|
if (view === this) continue;
|
|
898
974
|
const twin = view.objects.get(id);
|
|
@@ -900,9 +976,7 @@ export class BlenderRuntimeView {
|
|
|
900
976
|
twin.matrix.copy(object.matrix);
|
|
901
977
|
twin.matrix.decompose(twin.position, twin.quaternion, twin.scale);
|
|
902
978
|
twin.updateMatrixWorld(true);
|
|
903
|
-
moved = true;
|
|
904
979
|
}
|
|
905
|
-
if (moved) presenterChanged();
|
|
906
980
|
}
|
|
907
981
|
|
|
908
982
|
/** Subscribe to frames. Returns the unsubscribe. */
|
|
@@ -936,35 +1010,19 @@ export class BlenderRuntimeView {
|
|
|
936
1010
|
return null;
|
|
937
1011
|
}
|
|
938
1012
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
* The session exports both on every present, which is what makes the FRAME
|
|
944
|
-
* the state rather than a copy of it: an agent's `select_set` through the
|
|
945
|
-
* script door, an operator that activates what it just created, and a
|
|
946
|
-
* person's click in this editor all land in one place and come back the
|
|
947
|
-
* same way. Nothing in the tab holds a selection of its own —
|
|
948
|
-
* `blender-outliner-authoring.ts` reads this on every frame and overwrites
|
|
949
|
-
* the cache its panels render from.
|
|
950
|
-
*
|
|
951
|
-
* THE ACTIVE OBJECT IS REPORTED SEPARATELY because Blender's own state
|
|
952
|
-
* allows an active object that is not selected, and the Properties editor
|
|
953
|
-
* follows the ACTIVE one while the outline follows the selected set.
|
|
954
|
-
*/
|
|
1013
|
+
noteSavedShading(shading: keyof typeof SAVED_SHADING): void {
|
|
1014
|
+
if (this.frame?.view) this.frame.view.shading = shading;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
955
1017
|
/**
|
|
956
1018
|
* WHERE BLENDER OPENS THIS FILE: its saved 3D View in the stage's frame — the pivot, the
|
|
957
1019
|
* direction from the pivot to the eye (the view's +Z, `view_rotation` turning view space into
|
|
958
1020
|
* the world: RNA's inverse of `viewquat`), the screen's up (the view's +Y), the distance and
|
|
959
|
-
* the projection. A
|
|
960
|
-
*
|
|
1021
|
+
* the projection. A saved camera view supplies a perspective orbit pose along its saved axis;
|
|
1022
|
+
* `cameraView` separately provides the through-camera projection. With it the view's SHADING, the
|
|
961
1023
|
* draw mode Blender reopens it in (null from a file whose view carried none). Null before a
|
|
962
1024
|
* frame or when the file saved no 3D View.
|
|
963
1025
|
*/
|
|
964
|
-
noteSavedShading(shading: keyof typeof SAVED_SHADING): void {
|
|
965
|
-
if (this.frame?.view) this.frame.view.shading = shading;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
1026
|
savedView(): {
|
|
969
1027
|
readonly target: readonly [number, number, number];
|
|
970
1028
|
readonly direction: readonly [number, number, number];
|
|
@@ -975,6 +1033,8 @@ export class BlenderRuntimeView {
|
|
|
975
1033
|
readonly drawMode: 'wireframe' | 'solid' | 'preview' | 'rendered' | null;
|
|
976
1034
|
readonly sceneWorld: boolean;
|
|
977
1035
|
readonly sceneLights: boolean;
|
|
1036
|
+
/** Solid's and Wireframe's resolved background as an sRGB `#rrggbb`. */
|
|
1037
|
+
readonly background: string;
|
|
978
1038
|
} | null {
|
|
979
1039
|
const saved = this.frame?.view;
|
|
980
1040
|
if (!saved) return null;
|
|
@@ -992,9 +1052,20 @@ export class BlenderRuntimeView {
|
|
|
992
1052
|
drawMode: saved.shading ? SAVED_SHADING[saved.shading] : null,
|
|
993
1053
|
sceneWorld: saved.scene_world ?? false,
|
|
994
1054
|
sceneLights: saved.scene_lights ?? false,
|
|
1055
|
+
background: savedBackground(saved),
|
|
995
1056
|
};
|
|
996
1057
|
}
|
|
997
1058
|
|
|
1059
|
+
#playbackCamera: string | null = null;
|
|
1060
|
+
#playbackCameraData: Record<string, unknown> | null = null;
|
|
1061
|
+
/** The camera the scene's playback shows at the Timeline's frame (the markers' cut), and how it
|
|
1062
|
+
* stands there (its matrix and projection as the Timeline's mixer posed it); null: the scene's
|
|
1063
|
+
* own camera, as Blender last evaluated it. */
|
|
1064
|
+
setPlaybackCamera(name: string | null, data: Record<string, unknown> | null = null): void {
|
|
1065
|
+
this.#playbackCamera = name;
|
|
1066
|
+
this.#playbackCameraData = name === null ? null : data;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
998
1069
|
/**
|
|
999
1070
|
* THE CAMERA A CAMERA VIEW ENTERED NOW WOULD LOOK THROUGH, as `view_camera_exec` picks it:
|
|
1000
1071
|
* the scene's camera, else the active object when it is a camera, else the view layer's first
|
|
@@ -1004,6 +1075,11 @@ export class BlenderRuntimeView {
|
|
|
1004
1075
|
const frame = this.frame;
|
|
1005
1076
|
const view = frame?.camera_view;
|
|
1006
1077
|
if (!frame || !view) return null;
|
|
1078
|
+
// WHILE THE TIMELINE PLAYS OR SCRUBS, the camera of the marker at that frame (Blender switches
|
|
1079
|
+
// `scene.camera` at bound markers as it plays; the frame here is the one it last evaluated)
|
|
1080
|
+
// (described by the Timeline itself when Blender's last frame did not carry it: a present ships
|
|
1081
|
+
// only what moved)
|
|
1082
|
+
if (this.#playbackCamera !== null && (this.#playbackCameraData || frame.cameras[this.#playbackCamera])) return this.#playbackCamera;
|
|
1007
1083
|
if (view.scene_camera !== null && frame.cameras[view.scene_camera]) return view.scene_camera;
|
|
1008
1084
|
if (frame.active !== null && frame.cameras[frame.active]) return frame.active;
|
|
1009
1085
|
return view.view_layer_cameras.find((name) => frame.cameras[name]) ?? null;
|
|
@@ -1037,14 +1113,19 @@ export class BlenderRuntimeView {
|
|
|
1037
1113
|
offset: readonly [number, number],
|
|
1038
1114
|
): BlenderCameraView | null {
|
|
1039
1115
|
const frame = this.frame;
|
|
1040
|
-
const
|
|
1116
|
+
const evaluated = frame?.cameras[camera];
|
|
1117
|
+
// (while the Timeline plays, the camera as its mixer posed it at that frame)
|
|
1118
|
+
const posed = camera === this.#playbackCamera && this.#playbackCameraData
|
|
1119
|
+
? { ...((evaluated ?? {}) as Record<string, unknown>), ...this.#playbackCameraData } : evaluated;
|
|
1120
|
+
const data = cameraDataSchema.safeParse(posed);
|
|
1041
1121
|
if (!frame?.camera_view || !data.success) return null;
|
|
1042
1122
|
return blenderCameraView(data.data, this.root.matrix, region, zoom, offset, frame.camera_view.aspect);
|
|
1043
1123
|
}
|
|
1044
1124
|
|
|
1045
|
-
/**
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1125
|
+
/**
|
|
1126
|
+
* The viewport subject line at frame, prefixed by the frame and followed by its marker. A null
|
|
1127
|
+
* frame uses scene.frame_current; null output means no subject line is held.
|
|
1128
|
+
*/
|
|
1048
1129
|
subjectLine(playhead: number | null): string | null {
|
|
1049
1130
|
const subject = this.frame?.subject;
|
|
1050
1131
|
if (subject === undefined) return null;
|
|
@@ -1071,6 +1152,22 @@ export class BlenderRuntimeView {
|
|
|
1071
1152
|
return step[1];
|
|
1072
1153
|
}
|
|
1073
1154
|
|
|
1155
|
+
/**
|
|
1156
|
+
* BLENDER'S OWN SELECTION, AS THIS FRAME CARRIES IT — the per-object
|
|
1157
|
+
* `selected` flag and the scene's `active`, by Blender datablock NAME.
|
|
1158
|
+
*
|
|
1159
|
+
* The session exports both on every present, which is what makes the FRAME
|
|
1160
|
+
* the state rather than a copy of it: an agent's `select_set` through the
|
|
1161
|
+
* script door, an operator that activates what it just created, and a
|
|
1162
|
+
* person's click in this editor all land in one place and come back the
|
|
1163
|
+
* same way. Nothing in the tab holds a selection of its own —
|
|
1164
|
+
* `blender-outliner-authoring.ts` reads this on every frame and overwrites
|
|
1165
|
+
* the cache its panels render from.
|
|
1166
|
+
*
|
|
1167
|
+
* THE ACTIVE OBJECT IS REPORTED SEPARATELY because Blender's own state
|
|
1168
|
+
* allows an active object that is not selected, and the Properties editor
|
|
1169
|
+
* follows the ACTIVE one while the outline follows the selected set.
|
|
1170
|
+
*/
|
|
1074
1171
|
blenderSelection(): { readonly selected: readonly string[]; readonly active: string | null } {
|
|
1075
1172
|
const frame = this.frame;
|
|
1076
1173
|
if (frame === null) return { selected: [], active: null };
|
|
@@ -1080,20 +1177,7 @@ export class BlenderRuntimeView {
|
|
|
1080
1177
|
};
|
|
1081
1178
|
}
|
|
1082
1179
|
|
|
1083
|
-
/**
|
|
1084
|
-
* BLENDER'S MODE, PUBLISHED ON THE DOCUMENT'S OWN CONTEXT.
|
|
1085
|
-
*
|
|
1086
|
-
* `volter.stage.mode` (U6's context key, `editor-host-door.ts`'s `stage()`) had
|
|
1087
|
-
* no reporter at all: Edit Mesh went with the mesh kit and the Model document
|
|
1088
|
-
* had no mode strip, so every `when` clause that reads it was answering
|
|
1089
|
-
* `null`. The engine HAS a mode — `bpy.context.mode`, and `Object.mode` on the
|
|
1090
|
-
* active object, which is what the Outliner's own pose rows key on
|
|
1091
|
-
* (`tree_element_pose.cc`: the channels exist only in pose mode) — and the
|
|
1092
|
-
* TREE door reports both. So the document publishes it here, on the context it
|
|
1093
|
-
* already publishes (`publishContext(view)`), rather than through a second
|
|
1094
|
-
* door: `blender-outliner-model.ts` sets it on every read, and the host's
|
|
1095
|
-
* `stage()` asks the ACTIVE document's context for it.
|
|
1096
|
-
*/
|
|
1180
|
+
/** Publish bpy.context.mode and active-object mode on the document context for stage conditions and the Outliner. */
|
|
1097
1181
|
private mode: string | null = null;
|
|
1098
1182
|
|
|
1099
1183
|
setStageMode(mode: string | null): void {
|
|
@@ -1106,19 +1190,13 @@ export class BlenderRuntimeView {
|
|
|
1106
1190
|
|
|
1107
1191
|
/** PLAY IS A MODE OF THE STAGE: while the document plays, the keys are the player's, and the
|
|
1108
1192
|
* mode this context publishes is what the frame's key rules stand down on
|
|
1109
|
-
* (`
|
|
1193
|
+
* (`volter.stage.mode != 'PLAY'`). Blender's own mode returns on stop. */
|
|
1110
1194
|
private playing = false;
|
|
1111
1195
|
|
|
1112
1196
|
setPlaying(playing: boolean): void {
|
|
1113
1197
|
this.playing = playing;
|
|
1114
1198
|
}
|
|
1115
1199
|
|
|
1116
|
-
/** The inverse: the presented object for a Blender datablock NAME, or null
|
|
1117
|
-
* when this tab is not showing one (a camera, an excluded collection's
|
|
1118
|
-
* member, an object type the presenter draws nothing for). The OUTLINER
|
|
1119
|
-
* needs it — its rows come from the engine by name, and every one that our
|
|
1120
|
-
* viewport can select has to find its way back to a three object through
|
|
1121
|
-
* the frame's own table, never through `object.name`. */
|
|
1122
1200
|
/**
|
|
1123
1201
|
* WHAT THE LAST FRAME SAYS ABOUT ANIMATION, for a reader that plays clips on `skeletons`: the
|
|
1124
1202
|
* scene's current frame and clock, every action's revision, each armature's assigned action, the
|
|
@@ -1136,6 +1214,9 @@ export class BlenderRuntimeView {
|
|
|
1136
1214
|
* pose), its action and its NLA stack. */
|
|
1137
1215
|
armatures: Readonly<Record<string, BlenderArmature>>;
|
|
1138
1216
|
skinArmature(object: string): string | null;
|
|
1217
|
+
/** The scene's camera, every camera's settings and placement, as one string: the movie, which
|
|
1218
|
+
* carries them, is read again when it changes (switching the scene camera, an unkeyed lens). */
|
|
1219
|
+
cameras: string;
|
|
1139
1220
|
} {
|
|
1140
1221
|
const frame = this.frame;
|
|
1141
1222
|
return {
|
|
@@ -1145,6 +1226,11 @@ export class BlenderRuntimeView {
|
|
|
1145
1226
|
active: frame?.active ?? null,
|
|
1146
1227
|
actions: frame?.actions ?? {},
|
|
1147
1228
|
armatures: frame?.armatures ?? {},
|
|
1229
|
+
cameras: JSON.stringify([
|
|
1230
|
+
frame?.camera_view?.scene_camera ?? null,
|
|
1231
|
+
frame?.cameras ?? {},
|
|
1232
|
+
(frame?.objects ?? []).filter((o) => o.type === 'CAMERA').map((o) => [o.name, o.matrix]),
|
|
1233
|
+
]),
|
|
1148
1234
|
skinArmature: (name) => {
|
|
1149
1235
|
const object = this.objectForBlenderName(name) as THREE.Mesh | null;
|
|
1150
1236
|
const skin = object?.geometry?.userData['blenderSkin'] as { armature: string } | undefined;
|
|
@@ -1153,6 +1239,12 @@ export class BlenderRuntimeView {
|
|
|
1153
1239
|
};
|
|
1154
1240
|
}
|
|
1155
1241
|
|
|
1242
|
+
/** The inverse: the presented object for a Blender datablock NAME, or null
|
|
1243
|
+
* when this tab is not showing one (a camera, an excluded collection's
|
|
1244
|
+
* member, an object type the presenter draws nothing for). The OUTLINER
|
|
1245
|
+
* needs it — its rows come from the engine by name, and every one that our
|
|
1246
|
+
* viewport can select has to find its way back to a three object through
|
|
1247
|
+
* the frame's own table, never through `object.name`. */
|
|
1156
1248
|
objectForBlenderName(name: string): THREE.Object3D | null {
|
|
1157
1249
|
const entry = this.frame?.objects.find((o) => o.name === name);
|
|
1158
1250
|
return entry === undefined ? null : (this.objects.get(entry.id) ?? null);
|
|
@@ -1162,16 +1254,8 @@ export class BlenderRuntimeView {
|
|
|
1162
1254
|
* Put `next` where `previous` stood — same parent, same id in the object
|
|
1163
1255
|
* table, same place among its siblings' children.
|
|
1164
1256
|
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
* three has no way to promote a `Mesh` in place. It goes through this method
|
|
1168
|
-
* rather than the map directly because the table's KEY is the frame's object
|
|
1169
|
-
* id and only this class knows it — a swap that missed the table would be
|
|
1170
|
-
* replaced again by the next frame's reuse check, every frame, forever.
|
|
1171
|
-
*
|
|
1172
|
-
* A `SkinnedMesh` still answers `isMesh`, so the next frame's check
|
|
1173
|
-
* (`isMesh === (obj.mesh !== null)`) keeps it and merely re-points its
|
|
1174
|
-
* geometry and materials, which is exactly right.
|
|
1257
|
+
* Frame application and skeleton promotion use this door so replacements
|
|
1258
|
+
* retain the frame's object identity and the next frame can reuse them.
|
|
1175
1259
|
*/
|
|
1176
1260
|
replacePresentedObject(previous: THREE.Object3D, next: THREE.Object3D): void {
|
|
1177
1261
|
for (const [id, object] of this.objects)
|
|
@@ -1194,14 +1278,9 @@ export class BlenderRuntimeView {
|
|
|
1194
1278
|
this.instances.sync();
|
|
1195
1279
|
};
|
|
1196
1280
|
this.root.name = 'Model';
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
//
|
|
1200
|
-
// this matrix is the one a render inverts to answer with the pose it
|
|
1201
|
-
// photographed from (`blender-runtime-host.ts`). An exact signed axis
|
|
1202
|
-
// permutation inverts exactly, so that round trip is checkable with no
|
|
1203
|
-
// tolerance; the float quarter turn is not, and it also leaves every model
|
|
1204
|
-
// rotated by 89.999999999999996 degrees for nothing.
|
|
1281
|
+
// Map Blender Z-up into three.js Y-up with the exact signed permutation (x, y, z) → (x, z, -y).
|
|
1282
|
+
// Camera poses transformed through this matrix and its inverse round-trip without a
|
|
1283
|
+
// floating-point tolerance.
|
|
1205
1284
|
this.root.matrixAutoUpdate = false;
|
|
1206
1285
|
this.root.matrix.set(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1);
|
|
1207
1286
|
this.root.updateMatrixWorld(true);
|
|
@@ -1232,16 +1311,6 @@ export class BlenderRuntimeView {
|
|
|
1232
1311
|
this.emptyExtrasRoot.name = 'BlenderEmptyExtras';
|
|
1233
1312
|
}
|
|
1234
1313
|
|
|
1235
|
-
/**
|
|
1236
|
-
* THE OVERLAY GROUPS, for the document to hand to its stage.
|
|
1237
|
-
*
|
|
1238
|
-
* `kind` is the `HelperVisibility` member whose checkbox owns it: `skeletons`
|
|
1239
|
-
* is the Helpers menu's existing Skeletons row, which is Blender's viewport
|
|
1240
|
-
* overlay "Bones" checkbox (`View3DOverlay.show_bones`,
|
|
1241
|
-
* `rna_space.cc:5125-5129`; `space_view3d.py:7161`) under this editor's own
|
|
1242
|
-
* noun; `weights` is a member this unit added, because nothing in the set
|
|
1243
|
-
* stood for a vertex-group weight display.
|
|
1244
|
-
*/
|
|
1245
1314
|
/**
|
|
1246
1315
|
* BLENDER'S SOLID-MODE STUDIO, for the document to hand to its stage as
|
|
1247
1316
|
* `dressing.viewLocked`.
|
|
@@ -1257,6 +1326,15 @@ export class BlenderRuntimeView {
|
|
|
1257
1326
|
return this.lighting.group;
|
|
1258
1327
|
}
|
|
1259
1328
|
|
|
1329
|
+
/**
|
|
1330
|
+
* THE OVERLAY GROUPS, for the document to hand to its stage.
|
|
1331
|
+
*
|
|
1332
|
+
* `kind` is the `HelperVisibility` member whose checkbox owns it: `skeletons`
|
|
1333
|
+
* is the Helpers menu's Skeletons row, which is Blender's viewport
|
|
1334
|
+
* overlay "Bones" checkbox (`View3DOverlay.show_bones`,
|
|
1335
|
+
* `rna_space.cc:5125-5129`; `space_view3d.py:7161`) under this editor's own
|
|
1336
|
+
* noun; `weights` controls the vertex-group weight display.
|
|
1337
|
+
*/
|
|
1260
1338
|
overlayGroups(): readonly { kind: string; object: THREE.Object3D }[] {
|
|
1261
1339
|
return [
|
|
1262
1340
|
{ kind: 'skeletons', object: this.armatureRoot },
|
|
@@ -1287,6 +1365,7 @@ export class BlenderRuntimeView {
|
|
|
1287
1365
|
return placement;
|
|
1288
1366
|
}
|
|
1289
1367
|
|
|
1368
|
+
private heldSceneLighting: { world: boolean; lights: boolean } | undefined;
|
|
1290
1369
|
/**
|
|
1291
1370
|
* BLENDER'S RENDERED SHADING: the viewport held in the lighting a render photographs with
|
|
1292
1371
|
* ({@link setRendered}) — the scene's own lights, its World behind and around the model,
|
|
@@ -1296,7 +1375,6 @@ export class BlenderRuntimeView {
|
|
|
1296
1375
|
* keeps viewport visibility independently of its shading and lighting choices.
|
|
1297
1376
|
* A render taken meanwhile ends back in this state rather than in modeling.
|
|
1298
1377
|
*/
|
|
1299
|
-
private heldSceneLighting: { world: boolean; lights: boolean } | undefined;
|
|
1300
1378
|
holdRendered(drawCamera: (() => THREE.Camera) | null, sceneLighting?: { world: boolean; lights: boolean }, visibility: 'viewport' | 'render' = 'viewport'): void {
|
|
1301
1379
|
if (drawCamera === this.heldRendered && sceneLighting?.world === this.heldSceneLighting?.world && sceneLighting?.lights === this.heldSceneLighting?.lights && visibility === this.heldVisibility) return;
|
|
1302
1380
|
this.heldRendered = drawCamera;
|
|
@@ -1345,12 +1423,31 @@ export class BlenderRuntimeView {
|
|
|
1345
1423
|
}
|
|
1346
1424
|
|
|
1347
1425
|
private report(work: Promise<void>): void {
|
|
1426
|
+
this.renderedWork = work.then(() => undefined, () => undefined);
|
|
1348
1427
|
void work.then(presenterChanged, (error: unknown) =>
|
|
1349
1428
|
// biome-ignore lint/suspicious/noConsole: the editor console captures console.error session-wide; this is the report channel.
|
|
1350
1429
|
console.error(`Rendered shading could not be applied: ${error instanceof Error ? error.message : String(error)}`),
|
|
1351
1430
|
);
|
|
1352
1431
|
}
|
|
1353
1432
|
|
|
1433
|
+
/** The Rendered shading the viewport was last put in, still settling (`applyRendered`). */
|
|
1434
|
+
private renderedWork: Promise<void> = Promise.resolve();
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* WHAT THE FRAME ON SCREEN IS STILL WAITING ON before it draws as it will: Rendered shading's
|
|
1438
|
+
* settling (area-light tables, the World's sky) and every image's decode. A photograph through
|
|
1439
|
+
* {@link setRendered} waits for the same things; a view presented on the viewport waits here
|
|
1440
|
+
* (the model document's preparation), so a capture right after a model opens or Rendered is
|
|
1441
|
+
* entered does not catch any of them half-way. A graph program still compiling is the draw's
|
|
1442
|
+
* own business (`photographGraphs`).
|
|
1443
|
+
*/
|
|
1444
|
+
async drawReady(): Promise<void> {
|
|
1445
|
+
await this.renderedWork;
|
|
1446
|
+
await lightingReady();
|
|
1447
|
+
await Promise.allSettled([...this.textures.values()].map((held) => held.ready));
|
|
1448
|
+
await this.world.ready().catch(() => undefined);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1354
1451
|
/**
|
|
1355
1452
|
* BLENDER'S SOLID SHADING ON OR OFF: while on, every presented surface is drawn by Blender's
|
|
1356
1453
|
* Solid function over its material's viewport display; a render photograph, Material Preview
|
|
@@ -1363,6 +1460,29 @@ export class BlenderRuntimeView {
|
|
|
1363
1460
|
presenterChanged();
|
|
1364
1461
|
}
|
|
1365
1462
|
|
|
1463
|
+
/**
|
|
1464
|
+
* THE AUTHORED MATERIALS, whatever the viewport shows: `run` sees every presented mesh dressed in the
|
|
1465
|
+
* materials its object's slots name (what Material Preview and Rendered show), and each gets back what
|
|
1466
|
+
* it wore after. An export writes the model's own materials in Solid shading too (`blender-export-gltf`).
|
|
1467
|
+
*/
|
|
1468
|
+
async withAuthoredMaterials<T>(run: () => Promise<T>): Promise<T> {
|
|
1469
|
+
const worn = new Map<THREE.Mesh, THREE.Material | THREE.Material[]>();
|
|
1470
|
+
for (const obj of this.frame?.objects ?? []) {
|
|
1471
|
+
if (obj.mesh === null || obj.volume) continue;
|
|
1472
|
+
const mesh = this.objects.get(obj.id) as THREE.Mesh | undefined;
|
|
1473
|
+
if (!mesh?.isMesh) continue;
|
|
1474
|
+
const slots: readonly (string | null)[] = obj.materials.length ? obj.materials : [null];
|
|
1475
|
+
const authored = slots.map((id) => (id === null ? this.fallback : (this.materials.get(id) ?? this.fallback)));
|
|
1476
|
+
worn.set(mesh, mesh.material);
|
|
1477
|
+
mesh.material = obj.materials.length ? authored : authored[0]!;
|
|
1478
|
+
}
|
|
1479
|
+
try {
|
|
1480
|
+
return await run();
|
|
1481
|
+
} finally {
|
|
1482
|
+
for (const [mesh, material] of worn) mesh.material = material;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1366
1486
|
/** Each surface's material for the state: authored, or Solid's, derived from the frame every
|
|
1367
1487
|
* time so a replaced node (the skin's) is never left wearing the other. */
|
|
1368
1488
|
private applyWorkbench(): void {
|
|
@@ -1376,7 +1496,8 @@ export class BlenderRuntimeView {
|
|
|
1376
1496
|
const slots: readonly (string | null)[] = obj.materials.length ? obj.materials : [null];
|
|
1377
1497
|
const shown = slots.map((id) => {
|
|
1378
1498
|
const authored = id === null ? this.fallback : (this.materials.get(id) ?? this.fallback);
|
|
1379
|
-
|
|
1499
|
+
// Solid ignores a material's Backface Culling (the 3D View's own option governs it, off by default).
|
|
1500
|
+
return solid ? this.workbenchFor(id, THREE.DoubleSide, used) : this.objectInfoMaterials.get(authored,mesh);
|
|
1380
1501
|
});
|
|
1381
1502
|
mesh.material = obj.materials.length ? shown : shown[0]!;
|
|
1382
1503
|
prepareGraphGeometry(mesh);
|
|
@@ -1416,7 +1537,9 @@ export class BlenderRuntimeView {
|
|
|
1416
1537
|
|
|
1417
1538
|
/** Before the renderer uploads attributes/builds its queues, for this area's
|
|
1418
1539
|
* actual camera. Transparent instance runs are camera-order dependent. */
|
|
1419
|
-
prepareDraw(camera: THREE.Camera, options?:
|
|
1540
|
+
prepareDraw(camera: THREE.Camera, options?: BlenderDrawOptions): () => void {
|
|
1541
|
+
const outline = this.outlineColor(options);
|
|
1542
|
+
photographGraphs(options?.photograph === true);
|
|
1420
1543
|
try {
|
|
1421
1544
|
if (!this.workbench || this.rendered) {
|
|
1422
1545
|
if(this.objectInfoMaterials.needsRemap()) this.applyWorkbench();
|
|
@@ -1425,19 +1548,11 @@ export class BlenderRuntimeView {
|
|
|
1425
1548
|
const start = performance.now();
|
|
1426
1549
|
this.instances.prepareDraw(camera);
|
|
1427
1550
|
this.root.updateMatrixWorld(true);
|
|
1551
|
+
this.skeletons.fitBounds();
|
|
1428
1552
|
// The script/navigation has just posed the camera and the detached
|
|
1429
1553
|
// objects. Fit shadows for that draw, rather than the camera's pose
|
|
1430
1554
|
// when Rendered shading was first entered. Captures use this path too.
|
|
1431
|
-
if (this.rendered) this.applyShadows(true, camera);
|
|
1432
|
-
if (WORLD_VISIBILITY) {
|
|
1433
|
-
this.worldVisibility.sync(this.objects.values(),this.world.lightingTexture(),this.root,this.rendered);
|
|
1434
|
-
for(const material of [...this.materials.values(),this.fallback])bindWorldVisibility(material,this.worldVisibility);
|
|
1435
|
-
for(const object of this.objects.values()) {
|
|
1436
|
-
const mesh=object as THREE.Mesh;if(!mesh.isMesh)continue;
|
|
1437
|
-
for(const material of Array.isArray(mesh.material)?mesh.material:[mesh.material])bindWorldVisibility(material,this.worldVisibility);
|
|
1438
|
-
}
|
|
1439
|
-
if(options?.renderer)this.worldVisibility.prepare(options.renderer);
|
|
1440
|
-
}
|
|
1555
|
+
if (this.rendered) this.applyShadows(true, camera, options?.renderer, true);
|
|
1441
1556
|
const opaqueDone = performance.now();
|
|
1442
1557
|
const motion = this.motionGeometry.prepare(camera, !this.rendered && (this.mode ?? this.frame?.mode) === 'OBJECT' &&
|
|
1443
1558
|
options?.interactive === true, options?.height ?? 0);
|
|
@@ -1455,6 +1570,7 @@ export class BlenderRuntimeView {
|
|
|
1455
1570
|
this.drawPreparation.last = timing;
|
|
1456
1571
|
if (motion) this.drawPreparation.navigation = timing;
|
|
1457
1572
|
} catch (error) {
|
|
1573
|
+
photographGraphs(false);
|
|
1458
1574
|
this.materialRanges.finish();
|
|
1459
1575
|
this.motionGeometry.finish();
|
|
1460
1576
|
this.instances.finishDraw();
|
|
@@ -1462,30 +1578,108 @@ export class BlenderRuntimeView {
|
|
|
1462
1578
|
throw error;
|
|
1463
1579
|
}
|
|
1464
1580
|
return () => {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1581
|
+
try {
|
|
1582
|
+
// Over the frame as it was just drawn: its draw geometry and batches still stand.
|
|
1583
|
+
if (outline && options?.renderer) this.drawOutline(options.renderer, camera, outline, options.pixelScale ?? 1);
|
|
1584
|
+
} finally {
|
|
1585
|
+
photographGraphs(false);
|
|
1586
|
+
this.materialRanges.finish();
|
|
1587
|
+
this.motionGeometry.finish();
|
|
1588
|
+
this.instances.finishDraw();
|
|
1589
|
+
this.transparentInstances.finishDraw();
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/** Solid's object outline colour for this draw, or null when it draws none: Blender's Solid
|
|
1595
|
+
* with the file's `show_object_outline` on, and a renderer to draw it with. */
|
|
1596
|
+
private outlineColor(options?: BlenderDrawOptions): readonly [number, number, number] | null {
|
|
1597
|
+
const saved = this.frame?.view;
|
|
1598
|
+
if (!options?.renderer || options.drawMode !== 'solid' || !this.workbench || this.rendered) return null;
|
|
1599
|
+
if (!saved?.show_object_outline) return null;
|
|
1600
|
+
return saved.object_outline_color ?? [0, 0, 0];
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
private drawOutline(renderer: THREE.WebGLRenderer, camera: THREE.Camera, color: readonly [number, number, number], pixelScale: number): void {
|
|
1604
|
+
let scene: THREE.Object3D | null = this.root;
|
|
1605
|
+
while (scene && !(scene as THREE.Scene).isScene) scene = scene.parent;
|
|
1606
|
+
if (!scene) return;
|
|
1607
|
+
const presented = new Set(this.objects.values());
|
|
1608
|
+
// The presenter's own draws (batches) stand in for some objects; the id pass draws those
|
|
1609
|
+
// objects themselves, each with its own id, as Blender's id buffer holds them.
|
|
1610
|
+
const internal = this.root.children.filter((child) => (child as THREE.Mesh).isMesh && !presented.has(child));
|
|
1611
|
+
this.outline.draw(renderer, scene as THREE.Scene, camera, this.objects.values(),
|
|
1612
|
+
(object) => !this.hiddenSurfaceLayers.has(object), internal, color, pixelScale);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* HOW EACH SHADING TYPE IS ANTI-ALIASED, as Blender's viewport accumulates it
|
|
1617
|
+
* (`blender-viewport-sampling.ts` holds both models): a photograph draws every sample at once,
|
|
1618
|
+
* and the stage's live view one per redraw while nothing changes, as Blender's viewport does.
|
|
1619
|
+
* - SOLID: Workbench's accumulation at the Preferences' Viewport Anti-Aliasing
|
|
1620
|
+
* (`workbench_state.cc`: `U.viewport_aa` whatever the engine), whose factory value, 8, the tab
|
|
1621
|
+
* assumes: it holds no Blender preferences. Merged over its backdrop as the display shader
|
|
1622
|
+
* merges the overlay.
|
|
1623
|
+
* - RENDERED: Workbench at the scene's `display.viewport_aa` when Workbench is the engine ('OFF'
|
|
1624
|
+
* one centred sample, unfiltered: no anti-aliasing at all; 'FXAA' its one centred sample, the
|
|
1625
|
+
* SMAA Workbench lays over it not reproduced); EEVEE's film otherwise, at its viewport
|
|
1626
|
+
* `taa_samples` and `filter_size`.
|
|
1627
|
+
* - WIREFRAME: none here. Blender's wires are overlay lines smoothed by their own pass, not
|
|
1628
|
+
* accumulated, so a wireframe photograph keeps the stage's own supersampling and the live
|
|
1629
|
+
* view its MSAA.
|
|
1630
|
+
* - MATERIAL PREVIEW: EEVEE's film, whatever the engine.
|
|
1631
|
+
* Null before a frame.
|
|
1632
|
+
*/
|
|
1633
|
+
photographSampling(): Partial<Record<'solid' | 'wireframe' | 'preview' | 'rendered', BlenderPhotographSampling>> | null {
|
|
1634
|
+
const sampling = this.frame?.sampling;
|
|
1635
|
+
if (!sampling) return null;
|
|
1636
|
+
// The same answer for the same settings: the stage keeps its live accumulation while the
|
|
1637
|
+
// sampling it was begun by is the one it is handed (every Blender frame hands it again).
|
|
1638
|
+
const key = `${sampling.engine}|${sampling.viewport_aa}|${sampling.taa_samples}|${sampling.filter_size}`;
|
|
1639
|
+
if (this.#sampling?.key === key) return this.#sampling.value;
|
|
1640
|
+
const film = eeveeSampling(sampling.taa_samples, sampling.filter_size);
|
|
1641
|
+
const sceneAa = sampling.viewport_aa === 'OFF' ? 0 : sampling.viewport_aa === 'FXAA' ? 1 : Number.parseInt(sampling.viewport_aa, 10);
|
|
1642
|
+
const rendered = sampling.engine === 'BLENDER_WORKBENCH'
|
|
1643
|
+
? workbenchSampling(Number.isFinite(sceneAa) ? sceneAa : 1, false) : film;
|
|
1644
|
+
const value = { solid: workbenchSampling(PREFERENCES_VIEWPORT_AA, true), rendered, preview: film };
|
|
1645
|
+
this.#sampling = { key, value };
|
|
1646
|
+
return value;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
#sampling: { readonly key: string; readonly value: Partial<Record<'solid' | 'wireframe' | 'preview' | 'rendered', BlenderPhotographSampling>> } | null = null;
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* THE SCENE'S COLOUR MANAGEMENT for the frame on screen (`Scene.view_settings`, exposure as a
|
|
1653
|
+
* multiplier, and the display device), as `session.py` sends it with every present: Rendered and
|
|
1654
|
+
* Material Preview are drawn through it. Null before a frame, or from an engine that does not
|
|
1655
|
+
* send it.
|
|
1656
|
+
*/
|
|
1657
|
+
displaySettings(): (BlenderDisplaySettings & { readonly device: string }) | null {
|
|
1658
|
+
const display = this.frame?.display;
|
|
1659
|
+
if (!display) return null;
|
|
1660
|
+
return {
|
|
1661
|
+
transform: display.view_transform,
|
|
1662
|
+
look: display.look,
|
|
1663
|
+
exposure: 2 ** display.exposure,
|
|
1664
|
+
gamma: display.gamma,
|
|
1665
|
+
device: display.display_device,
|
|
1469
1666
|
};
|
|
1470
1667
|
}
|
|
1471
1668
|
|
|
1472
1669
|
private workbenchFor(id: string | null, side: THREE.Side, used: Set<string>): THREE.Material {
|
|
1473
1670
|
const display = (id !== null ? this.frame?.materials[id]?.viewport : undefined) ?? DEFAULT_VIEWPORT_DISPLAY;
|
|
1474
|
-
|
|
1671
|
+
// Solid's Specular Lighting, as the file's 3D View holds it (on in a factory file).
|
|
1672
|
+
const specular = this.frame?.view?.show_specular_highlight ?? true;
|
|
1673
|
+
const key = `${display.color.join(',')}|${display.roughness}|${display.metallic}|${side}|${specular}`;
|
|
1475
1674
|
used.add(key);
|
|
1476
1675
|
let material = this.workbenchMaterials.get(key);
|
|
1477
1676
|
if (!material) {
|
|
1478
|
-
material = workbenchMaterial(display, side);
|
|
1677
|
+
material = workbenchMaterial(display, side, specular);
|
|
1479
1678
|
this.workbenchMaterials.set(key, material);
|
|
1480
1679
|
}
|
|
1481
1680
|
return material;
|
|
1482
1681
|
}
|
|
1483
1682
|
|
|
1484
|
-
/** Whether the viewport is held in render lighting (Blender's Rendered shading). */
|
|
1485
|
-
renderedHeld(): boolean {
|
|
1486
|
-
return this.heldRendered !== null;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
1683
|
/**
|
|
1490
1684
|
* Light the model the way a RENDER is lit — by the scene's own lights —
|
|
1491
1685
|
* rather than the way modeling is, by the studio key. `bpy.ops.render.render()`
|
|
@@ -1527,12 +1721,7 @@ export class BlenderRuntimeView {
|
|
|
1527
1721
|
this.applyVisibility();
|
|
1528
1722
|
this.applyWorkbench();
|
|
1529
1723
|
this.applyShadows(rendered, camera);
|
|
1530
|
-
//
|
|
1531
|
-
// first made every one of these a no-op on the first render: `apply` had
|
|
1532
|
-
// not started the sky yet, so `worldReady()` saw nothing pending and the
|
|
1533
|
-
// photograph went out with an empty sky texture. Measured -- a sky the page
|
|
1534
|
-
// had never derived returned a render in 0.08s, which is not fast, it is
|
|
1535
|
-
// wrong. One photograph, no second chance at it.
|
|
1724
|
+
// Await preparation after apply registers its sky work, so ready observes every pending texture.
|
|
1536
1725
|
if (rendered) {
|
|
1537
1726
|
await lightingReady();
|
|
1538
1727
|
await Promise.all([...this.textures.values()].map((held) => held.ready));
|
|
@@ -1544,27 +1733,17 @@ export class BlenderRuntimeView {
|
|
|
1544
1733
|
}
|
|
1545
1734
|
|
|
1546
1735
|
/**
|
|
1547
|
-
*
|
|
1548
|
-
*
|
|
1549
|
-
* MODELING reads the viewport's visibility and lights nothing the scene
|
|
1550
|
-
* owns; a RENDER reads `hide_render` — the flag the Cycles path's own
|
|
1551
|
-
* snapshot skipped objects by — and lights exactly the scene's lights.
|
|
1552
|
-
*/
|
|
1553
|
-
/**
|
|
1554
|
-
* Shadows, for a render only.
|
|
1555
|
-
*
|
|
1556
|
-
* The renderer's shadow map is already on (`StageHost`); what is
|
|
1557
|
-
* missing is anything opting into it, because MODELING wants none — a
|
|
1558
|
-
* shadow across the model is in the way while it is being built, and the
|
|
1559
|
-
* viewport has never cast one. So the meshes opt in for the photograph and
|
|
1560
|
-
* back out afterwards, and every shadow camera is fitted to the model,
|
|
1561
|
-
* whose size no default could know.
|
|
1736
|
+
* Enable mesh shadows for rendered shading and captures, and fit each
|
|
1737
|
+
* light's shadow camera to the visible receivers and eligible casters.
|
|
1562
1738
|
*/
|
|
1563
|
-
private applyShadows(rendered: boolean, camera?: THREE.Camera): void {
|
|
1564
|
-
|
|
1739
|
+
private applyShadows(rendered: boolean, camera?: THREE.Camera, renderer?: THREE.WebGLRenderer, posed = false): void {
|
|
1740
|
+
// When posed is true, prepareDraw already refreshed world matrices; reuse them.
|
|
1741
|
+
if (!posed) this.root.updateMatrixWorld(true);
|
|
1565
1742
|
this.world.updateSolarDirections(this.root);
|
|
1566
1743
|
const fitted = this.shadowFit;
|
|
1567
|
-
|
|
1744
|
+
// A fit made without a renderer (entering Rendered) is fitted again by the first draw,
|
|
1745
|
+
// which can see what the camera sees.
|
|
1746
|
+
if (rendered && fitted && fitted.camera === camera && (fitted.visible || !renderer || !camera) &&
|
|
1568
1747
|
(!camera || (fitted.cameraMatrix?.equals(camera.matrixWorld) && fitted.projection?.equals(camera.projectionMatrix))) &&
|
|
1569
1748
|
fitted.solarLights.length === this.world.solarLights.length &&
|
|
1570
1749
|
fitted.solarLights.every((light, index) => light === this.world.solarLights[index]) &&
|
|
@@ -1575,6 +1754,7 @@ export class BlenderRuntimeView {
|
|
|
1575
1754
|
})) return;
|
|
1576
1755
|
this.shadowFit = rendered ? {
|
|
1577
1756
|
camera,
|
|
1757
|
+
visible: renderer !== undefined && camera !== undefined,
|
|
1578
1758
|
cameraMatrix: camera?.matrixWorld.clone(),
|
|
1579
1759
|
projection: camera?.projectionMatrix.clone(),
|
|
1580
1760
|
solarLights: [...this.world.solarLights],
|
|
@@ -1603,14 +1783,45 @@ export class BlenderRuntimeView {
|
|
|
1603
1783
|
new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse),
|
|
1604
1784
|
)
|
|
1605
1785
|
: null;
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1786
|
+
const suns = [...this.lights.values(), ...this.world.solarLights]
|
|
1787
|
+
.some((light) => (light as THREE.DirectionalLight).isDirectionalLight && light.castShadow);
|
|
1788
|
+
// WHAT THE CAMERA SEES is what a sun's map covers, as EEVEE tags its pages from the visible
|
|
1789
|
+
// surfaces (`VisibleReceivers`); the frustum's boxes only when no renderer is at hand to look.
|
|
1790
|
+
const seen = suns && renderer && camera ? this.sampleVisibleReceivers(renderer, camera) : [];
|
|
1791
|
+
// Every visible receiver is needed only to fit a sun without a renderer to look; a point or spot
|
|
1792
|
+
// light's bias needs only the farthest one (`farthestVisibleReceiver`).
|
|
1793
|
+
let clipped: THREE.Vector3[] | null = null;
|
|
1794
|
+
const receivers = (): THREE.Vector3[] => seen.length
|
|
1795
|
+
? seen
|
|
1796
|
+
: frustum
|
|
1797
|
+
? (clipped ??= boxes.flatMap((bounds) => visibleShadowReceivers(bounds, frustum)))
|
|
1798
|
+
: [];
|
|
1609
1799
|
for (const light of [...this.lights.values(), ...this.world.solarLights]) {
|
|
1610
1800
|
if ((light as THREE.DirectionalLight).isDirectionalLight && camera)
|
|
1611
|
-
fitModelDirectionalShadow(light as THREE.DirectionalLight, receivers, casters);
|
|
1612
|
-
else
|
|
1801
|
+
fitModelDirectionalShadow(light as THREE.DirectionalLight, light.castShadow ? receivers() : [], casters);
|
|
1802
|
+
else {
|
|
1803
|
+
fitShadow(light, sphere.center, sphere.radius);
|
|
1804
|
+
if ((light as THREE.PointLight).isPointLight || (light as THREE.SpotLight).isSpotLight)
|
|
1805
|
+
fitPerspectiveShadowBias(light as THREE.PointLight | THREE.SpotLight, seen.length || !frustum ? receivers()
|
|
1806
|
+
: light.castShadow ? farthestVisibleReceiver(boxes, frustum, light.getWorldPosition(new THREE.Vector3())) : []);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/** The visible surfaces through `camera`, leaving out what the drawing sees through. */
|
|
1812
|
+
private sampleVisibleReceivers(renderer: THREE.WebGLRenderer, camera: THREE.Camera): THREE.Vector3[] {
|
|
1813
|
+
let scene: THREE.Object3D | null = this.root;
|
|
1814
|
+
while (scene && !(scene as THREE.Scene).isScene) scene = scene.parent;
|
|
1815
|
+
if (!scene) return [];
|
|
1816
|
+
const seeThrough: THREE.Object3D[] = [];
|
|
1817
|
+
for (const object of this.objects.values()) {
|
|
1818
|
+
const mesh = object as THREE.Mesh;
|
|
1819
|
+
if (!mesh.isMesh) continue;
|
|
1820
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
1821
|
+
if (materials.every((material) => material.transparent || (material as THREE.MeshPhysicalMaterial).transmission > 0))
|
|
1822
|
+
seeThrough.push(mesh);
|
|
1613
1823
|
}
|
|
1824
|
+
return this.visibleReceivers.sample(renderer, scene as THREE.Scene, camera, seeThrough);
|
|
1614
1825
|
}
|
|
1615
1826
|
|
|
1616
1827
|
/**
|
|
@@ -1708,6 +1919,8 @@ export class BlenderRuntimeView {
|
|
|
1708
1919
|
});
|
|
1709
1920
|
}
|
|
1710
1921
|
|
|
1922
|
+
/** Use render visibility for captures and views that request it. Editing
|
|
1923
|
+
* retains viewport visibility independently of its shading and lighting. */
|
|
1711
1924
|
private applyVisibility(): void {
|
|
1712
1925
|
// Restore canonical masks before suppressing an instancer's own surface.
|
|
1713
1926
|
// Its geometry can be hidden while its placed children remain visible.
|
|
@@ -1765,10 +1978,9 @@ export class BlenderRuntimeView {
|
|
|
1765
1978
|
const data = exportedMeshSchema.parse(part.piece);
|
|
1766
1979
|
if (staged.meshes.has(part.mesh)) throw new Error('Duplicate Blender mesh piece');
|
|
1767
1980
|
const signature = `revision:${data.revision}`;
|
|
1768
|
-
const geometry = geometryFromDrawArrays(drawArraysFromColumns({
|
|
1981
|
+
const geometry = drawnOrRefused(() => withFrameSkin(geometryFromDrawArrays(drawArraysFromColumns({
|
|
1769
1982
|
...data.columns, attributes: data.attributes as never, activeUv: data.activeUv, renderUv: data.renderUv,
|
|
1770
|
-
}, signature));
|
|
1771
|
-
if (data.skin) skinGeometry(geometry, data.skin);
|
|
1983
|
+
}, signature)), data.skin));
|
|
1772
1984
|
staged.meshes.set(part.mesh, { signature, geometry });
|
|
1773
1985
|
} else if (part.image !== undefined) {
|
|
1774
1986
|
if (staged.images.has(part.image)) throw new Error('Duplicate Blender image piece');
|
|
@@ -1856,7 +2068,7 @@ export class BlenderRuntimeView {
|
|
|
1856
2068
|
// BufferGeometry stands. If it is not resident the worker's record
|
|
1857
2069
|
// is ahead of this presenter — a page reloaded under a still-running
|
|
1858
2070
|
// session — and saying so by name is what makes it re-send the frame
|
|
1859
|
-
// in full
|
|
2071
|
+
// in full.
|
|
1860
2072
|
// AND AT THAT REVISION: the frame names every mesh with the revision the engine holds
|
|
1861
2073
|
// it at, so it is a whole manifest of what this presenter should hold, and a resident
|
|
1862
2074
|
// geometry at another revision has drifted from the engine (a present the session
|
|
@@ -1890,7 +2102,7 @@ export class BlenderRuntimeView {
|
|
|
1890
2102
|
continue;
|
|
1891
2103
|
prepared.set(id, {
|
|
1892
2104
|
signature,
|
|
1893
|
-
geometry: exported
|
|
2105
|
+
geometry: drawnOrRefused(() => exported
|
|
1894
2106
|
? withFrameSkin(geometryFromDrawArrays(
|
|
1895
2107
|
drawArraysFromColumns(
|
|
1896
2108
|
{
|
|
@@ -1904,7 +2116,7 @@ export class BlenderRuntimeView {
|
|
|
1904
2116
|
), data.skin)
|
|
1905
2117
|
: drawn
|
|
1906
2118
|
? withDrawnSkin(geometryFromDrawArrays(data), data.skin)
|
|
1907
|
-
: drawRuntimeGeometry(data),
|
|
2119
|
+
: drawRuntimeGeometry(data)),
|
|
1908
2120
|
});
|
|
1909
2121
|
}
|
|
1910
2122
|
} catch (error) {
|
|
@@ -1939,7 +2151,7 @@ export class BlenderRuntimeView {
|
|
|
1939
2151
|
}
|
|
1940
2152
|
// THE PICTURES THE FRAME BROUGHT, before any material can ask for one. An
|
|
1941
2153
|
// entry is here only because this session has not sent these bytes at this
|
|
1942
|
-
// revision yet (`
|
|
2154
|
+
// revision yet (`session.py`'s image-carry record); everything else the
|
|
1943
2155
|
// materials name is already resident, which is the whole point of the
|
|
1944
2156
|
// revision.
|
|
1945
2157
|
for (const [name, data] of Object.entries(next.images)) {
|
|
@@ -1999,27 +2211,22 @@ export class BlenderRuntimeView {
|
|
|
1999
2211
|
});
|
|
2000
2212
|
}
|
|
2001
2213
|
for (const [id, data] of Object.entries(next.materials)) {
|
|
2002
|
-
// Cycles
|
|
2003
|
-
//
|
|
2004
|
-
//
|
|
2005
|
-
// there: a two-sided depth pass puts every lit face into its own map, and
|
|
2006
|
-
// the normal offset cannot hold that off at grazing light.
|
|
2214
|
+
// Render both surface faces, as Cycles does. Cast shadows from both faces unless the material
|
|
2215
|
+
// enables shadow backface culling. Fit the sun map to visible receivers and scale its normal
|
|
2216
|
+
// offset by the map’s texel size.
|
|
2007
2217
|
const material = this.materials.get(id) ??
|
|
2008
|
-
new THREE.MeshPhysicalMaterial({ side: THREE.DoubleSide, shadowSide: THREE.
|
|
2218
|
+
new THREE.MeshPhysicalMaterial({ side: THREE.DoubleSide, shadowSide: THREE.DoubleSide });
|
|
2219
|
+
material.shadowSide = data.backface_culling_shadow ? THREE.FrontSide : THREE.DoubleSide;
|
|
2220
|
+
// Match Blender’s face passes: cull back faces when declared, and draw blended surfaces in one pass with Transparency Overlap enabled.
|
|
2221
|
+
const side = data.backface_culling ? THREE.FrontSide : THREE.DoubleSide;
|
|
2222
|
+
const singlePass = data.transparency_overlap !== false;
|
|
2223
|
+
if (material.side !== side || material.forceSinglePass !== singlePass) {
|
|
2224
|
+
material.side = side;
|
|
2225
|
+
material.forceSinglePass = singlePass;
|
|
2226
|
+
material.needsUpdate = true;
|
|
2227
|
+
}
|
|
2009
2228
|
material.name = data.name;
|
|
2010
|
-
// A
|
|
2011
|
-
// it. Python's `_reduce` fills `color` from the Base Color socket's
|
|
2012
|
-
// default even when the socket is LINKED (the socket keeps its last
|
|
2013
|
-
// constant, and native never draws it), while three multiplies `map` by
|
|
2014
|
-
// `material.color` -- so carrying that constant alongside a map tinted
|
|
2015
|
-
// every textured surface by a leftover. Measured on `20-rigged-courier`:
|
|
2016
|
-
// the painted face's skin photographed (107,59,41) against native's
|
|
2017
|
-
// (154,108,77) at the same pixel, from a texture whose own skin is
|
|
2018
|
-
// (168,118,84) on both sides.
|
|
2019
|
-
//
|
|
2020
|
-
// With a map, the only legitimate multiplier is the `tint` a MULTIPLY mix
|
|
2021
|
-
// carries; without one it is white. Only a material with NO map draws the
|
|
2022
|
-
// constant.
|
|
2229
|
+
// A linked Base Color replaces the socket constant. Textured materials multiply only by the tint carried by a MULTIPLY mix; untextured materials use the constant.
|
|
2023
2230
|
const tint = data.texture?.tint;
|
|
2024
2231
|
if (data.texture !== undefined) {
|
|
2025
2232
|
material.color.setRGB(
|
|
@@ -2117,10 +2324,7 @@ export class BlenderRuntimeView {
|
|
|
2117
2324
|
material.needsUpdate = true;
|
|
2118
2325
|
}
|
|
2119
2326
|
}
|
|
2120
|
-
//
|
|
2121
|
-
// socket's default and the image never reached a pixel -- measured as a
|
|
2122
|
-
// byte-identical render with and without one. Blender's Base Color is an
|
|
2123
|
-
// sRGB-encoded image, so the texture says so and three linearises it.
|
|
2327
|
+
// Base Color images are sRGB; declare that encoding so three linearizes their texels.
|
|
2124
2328
|
const wanted = data.texture?.image.name ?? null;
|
|
2125
2329
|
if (wanted === null) {
|
|
2126
2330
|
if (this.textureNames.has(id)) {
|
|
@@ -2304,17 +2508,13 @@ export class BlenderRuntimeView {
|
|
|
2304
2508
|
if (bound && rig) this.skinBinds.set(bound, { geometry: bound.geometry, rig });
|
|
2305
2509
|
}
|
|
2306
2510
|
this.root.updateMatrixWorld(true);
|
|
2307
|
-
// Keep
|
|
2308
|
-
// a presented mesh is already resident as its BufferGeometry, and the
|
|
2309
|
-
// nested number arrays it arrived as cost ~15x their JSON (the blower's
|
|
2310
|
-
// last frame held 850 MB of main-thread heap this way, measured
|
|
2311
|
-
// 2026-09-13). Reuse checks compare the per-mesh signatures kept above.
|
|
2511
|
+
// Keep frame identity and object metadata after presentation; BufferGeometry owns the geometry bytes and per-mesh signatures support reuse checks.
|
|
2312
2512
|
this.frame = { ...next, meshes: {}, volumes: {}, images: {} };
|
|
2313
2513
|
this.worldKey = JSON.stringify(next.world ?? null);
|
|
2314
2514
|
// THE OVERLAYS, after the graph stands: the weight drawing is laid over
|
|
2315
2515
|
// the presented mesh's own geometry and the bones over the armature
|
|
2316
2516
|
// OBJECT's Blender matrix, so both need this frame's objects in place.
|
|
2317
|
-
const overlayWarnings = this.applyOverlays(next);
|
|
2517
|
+
const overlayWarnings = [...this.applyOverlays(next), ...this.nameRefusals(next.objects)];
|
|
2318
2518
|
if (overlayWarnings.length)
|
|
2319
2519
|
this.frame = { ...this.frame, warnings: [...this.frame.warnings, ...overlayWarnings] };
|
|
2320
2520
|
// Visibility is read off the frame, so it is applied once the frame stands.
|
|
@@ -2338,6 +2538,36 @@ export class BlenderRuntimeView {
|
|
|
2338
2538
|
return { ...this.inspect(), held };
|
|
2339
2539
|
}
|
|
2340
2540
|
|
|
2541
|
+
/** The meshes this presenter holds but could not draw, by mesh id, with the reason. */
|
|
2542
|
+
refusedMeshes(): Record<string, string> {
|
|
2543
|
+
const out: Record<string, string> = {};
|
|
2544
|
+
for (const [id, entry] of this.meshes) {
|
|
2545
|
+
const reason = entry.geometry.userData[REFUSED_GEOMETRY];
|
|
2546
|
+
if (typeof reason === 'string') out[id] = reason;
|
|
2547
|
+
}
|
|
2548
|
+
return out;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
/** Every held mesh that could not be drawn, named with the objects that show it: a frame
|
|
2552
|
+
* warning each frame, and on the editor's console once per mesh, revision and reason. */
|
|
2553
|
+
private nameRefusals(objects: readonly { name: string; mesh: string | null }[]): string[] {
|
|
2554
|
+
const said: string[] = [];
|
|
2555
|
+
for (const [id, reason] of Object.entries(this.refusedMeshes())) {
|
|
2556
|
+
const users = objects.filter((obj) => obj.mesh === id).map((obj) => JSON.stringify(obj.name));
|
|
2557
|
+
const shown = users.length > 8 ? `${users.slice(0, 8).join(', ')} and ${users.length - 8} more` : users.join(', ');
|
|
2558
|
+
const line = `The presenter skipped mesh ${JSON.stringify(id)}` +
|
|
2559
|
+
(users.length ? ` (${users.length > 1 ? 'objects' : 'object'} ${shown})` : '') +
|
|
2560
|
+
`, which it cannot draw: ${reason}. The rest of the document presents; the mesh is drawn again once it changes.`;
|
|
2561
|
+
said.push(line);
|
|
2562
|
+
const key = `${id}\u0000${this.meshes.get(id)?.signature}\u0000${reason}`;
|
|
2563
|
+
if (NAMED_REFUSALS.has(key)) continue;
|
|
2564
|
+
NAMED_REFUSALS.add(key);
|
|
2565
|
+
// biome-ignore lint/suspicious/noConsole: the editor console captures console.warn session-wide; this is the report channel.
|
|
2566
|
+
console.warn(`[blender] @@VOLTER-WARN ${line}`);
|
|
2567
|
+
}
|
|
2568
|
+
return said;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2341
2571
|
inspect() {
|
|
2342
2572
|
return {
|
|
2343
2573
|
session: this.frame?.session,
|
|
@@ -2357,31 +2587,30 @@ export class BlenderRuntimeView {
|
|
|
2357
2587
|
}
|
|
2358
2588
|
|
|
2359
2589
|
drawStatistics() {
|
|
2360
|
-
return {enabled: this.drawBatching, preparation: {...this.drawPreparation},
|
|
2590
|
+
return {enabled: this.drawBatching, preparation: {...this.drawPreparation}, materialRanges: this.materialRanges.inspect(), opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
|
|
2361
2591
|
}
|
|
2362
2592
|
|
|
2363
2593
|
snapshot() {
|
|
2364
2594
|
return this.frame;
|
|
2365
2595
|
}
|
|
2366
2596
|
|
|
2367
|
-
/**
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2597
|
+
/** The frame this view holds with no objects in it: its World, render and view settings. */
|
|
2598
|
+
private worldFrame(): Frame {
|
|
2599
|
+
const source = this.frame;
|
|
2600
|
+
if (!source) throw new Error('Blender capture requires a presented frame');
|
|
2601
|
+
return structuredClone({ ...source, objects: [], meshes: {}, materials: {}, instances: [], lights: {}, volumes: {}, images: {}, armatures: {}, weights: null });
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2374
2604
|
/** Everything this view holds, as ONE self-contained frame another view can be built from:
|
|
2375
2605
|
* every mesh's resident geometry and every resident image, with no reference to what a
|
|
2376
2606
|
* receiver already holds. */
|
|
2377
|
-
private fullFrame(
|
|
2607
|
+
private fullFrame(): Frame {
|
|
2378
2608
|
const source = this.frame;
|
|
2379
2609
|
if (!source) throw new Error('Blender capture requires a presented frame');
|
|
2380
2610
|
// Inspection overlays are not render content. In particular weight-paint
|
|
2381
2611
|
// overlays reference source vertices, which the evaluated draw no longer
|
|
2382
|
-
// needs and a render snapshot deliberately does not copy.
|
|
2383
|
-
|
|
2384
|
-
const frame: Frame = structuredClone({ ...source, images: {}, armatures: options.sourceVertices ? source.armatures : {}, weights: null });
|
|
2612
|
+
// needs and a render snapshot deliberately does not copy. A render snapshot draws no bones.
|
|
2613
|
+
const frame: Frame = structuredClone({ ...source, images: {}, armatures: {}, weights: null });
|
|
2385
2614
|
for (const [id, { signature, geometry }] of this.meshes) {
|
|
2386
2615
|
const attribute = (name: string): Float32Array<ArrayBuffer> | null => {
|
|
2387
2616
|
const value = geometry.getAttribute(name);
|
|
@@ -2417,20 +2646,6 @@ export class BlenderRuntimeView {
|
|
|
2417
2646
|
count: group.count,
|
|
2418
2647
|
materialIndex: group.materialIndex ?? 0,
|
|
2419
2648
|
})),
|
|
2420
|
-
// A COPY THAT PLAYS keeps the draw's vertex map: Play binds skins over it (a Blender
|
|
2421
|
-
// vertex's weights expand onto its drawn vertices through it), where a render snapshot
|
|
2422
|
-
// needs none.
|
|
2423
|
-
...(options.sourceVertices && geometry.getAttribute('blenderVertex')
|
|
2424
|
-
? { sourceVertex: new Uint32Array((geometry.getAttribute('blenderVertex') as THREE.BufferAttribute).array) }
|
|
2425
|
-
: {}),
|
|
2426
|
-
// A COPY THAT PLAYS keeps its skin too, per drawn vertex, so its own skeletons bind it.
|
|
2427
|
-
...(options.sourceVertices && geometry.userData['blenderSkin'] && geometry.getAttribute('skinIndex')
|
|
2428
|
-
? { skin: {
|
|
2429
|
-
...(geometry.userData['blenderSkin'] as { armature: string; groups: string[] }),
|
|
2430
|
-
index: new Uint16Array((geometry.getAttribute('skinIndex') as THREE.BufferAttribute).array),
|
|
2431
|
-
weight: new Float32Array((geometry.getAttribute('skinWeight') as THREE.BufferAttribute).array),
|
|
2432
|
-
} }
|
|
2433
|
-
: {}),
|
|
2434
2649
|
};
|
|
2435
2650
|
}
|
|
2436
2651
|
for (const [id, { signature }] of this.volumes)
|
|
@@ -2459,7 +2674,7 @@ export class BlenderRuntimeView {
|
|
|
2459
2674
|
colorspace: held.encoded.colorspace,
|
|
2460
2675
|
};
|
|
2461
2676
|
} else {
|
|
2462
|
-
const data = held.texture.image?.data;
|
|
2677
|
+
const data = (held.texture.image as { data?: unknown } | null | undefined)?.data;
|
|
2463
2678
|
if (!(data instanceof Uint8Array))
|
|
2464
2679
|
throw new Error(`Blender capture image ${name} has no resident raster`);
|
|
2465
2680
|
frame.images[name] = {
|
|
@@ -2503,13 +2718,13 @@ export class BlenderRuntimeView {
|
|
|
2503
2718
|
}
|
|
2504
2719
|
|
|
2505
2720
|
/**
|
|
2506
|
-
*
|
|
2507
|
-
*
|
|
2508
|
-
*
|
|
2721
|
+
* THE WORLD OF THE MODEL, FOR PLAY: a view of the World and the scene's settings this view holds now, with none of
|
|
2722
|
+
* the model's objects (a game over the standard artifact brings its own, RFC 0007), handed nothing afterwards.
|
|
2723
|
+
* Blender's data and this view never see it, and disposing it is the whole undo.
|
|
2509
2724
|
*/
|
|
2510
2725
|
detach(): { readonly view: BlenderRuntimeView; dispose(): void } {
|
|
2511
2726
|
const detached = new BlenderRuntimeView();
|
|
2512
|
-
if (this.frame) detached.applyFrame(this.
|
|
2727
|
+
if (this.frame) detached.applyFrame(this.worldFrame());
|
|
2513
2728
|
let disposed = false;
|
|
2514
2729
|
return {
|
|
2515
2730
|
view: detached,
|
|
@@ -2522,6 +2737,13 @@ export class BlenderRuntimeView {
|
|
|
2522
2737
|
};
|
|
2523
2738
|
}
|
|
2524
2739
|
|
|
2740
|
+
/**
|
|
2741
|
+
* A revision-owned render presenter, never a clone of the mounted scene.
|
|
2742
|
+
* Copy the resident draw arrays and image bytes synchronously, then rebuild
|
|
2743
|
+
* through the same frame contract. No live objects, callbacks, textures or
|
|
2744
|
+
* disposable geometry cross this boundary; delta-only frames are sufficient.
|
|
2745
|
+
* The caller owns this snapshot and must dispose it on every outcome.
|
|
2746
|
+
*/
|
|
2525
2747
|
captureSnapshot() {
|
|
2526
2748
|
const frame = this.fullFrame();
|
|
2527
2749
|
const source = this.frame!;
|
|
@@ -2556,7 +2778,7 @@ export class BlenderRuntimeView {
|
|
|
2556
2778
|
},
|
|
2557
2779
|
prepareDraw:(renderer:THREE.WebGLRenderer,camera:THREE.Camera)=>{
|
|
2558
2780
|
if(disposed)throw new Error('Blender capture snapshot is disposed');
|
|
2559
|
-
return detached.prepareDraw(camera,{interactive:false,height:renderer.domElement.height,renderer});
|
|
2781
|
+
return detached.prepareDraw(camera,{interactive:false,height:renderer.domElement.height,renderer,photograph:true});
|
|
2560
2782
|
},
|
|
2561
2783
|
dispose,
|
|
2562
2784
|
};
|
|
@@ -2584,7 +2806,7 @@ export class BlenderRuntimeView {
|
|
|
2584
2806
|
},
|
|
2585
2807
|
prepareDraw: (renderer, camera) => {
|
|
2586
2808
|
if (disposed) throw new Error('Blender capture snapshot is disposed');
|
|
2587
|
-
return this.prepareDraw(camera, { interactive: false, height: renderer.domElement.height, renderer });
|
|
2809
|
+
return this.prepareDraw(camera, { interactive: false, height: renderer.domElement.height, renderer, photograph: true });
|
|
2588
2810
|
},
|
|
2589
2811
|
dispose: () => {
|
|
2590
2812
|
if (disposed) return;
|
|
@@ -2632,9 +2854,8 @@ export class BlenderRuntimeView {
|
|
|
2632
2854
|
}
|
|
2633
2855
|
|
|
2634
2856
|
/** THE OBSERVATION DOOR onto both records, for whoever is grading this tab
|
|
2635
|
-
* from outside
|
|
2636
|
-
* reads it
|
|
2637
|
-
* replay compares). Nothing in the product reads it. */
|
|
2857
|
+
* from outside through the editor's document REPL. Nothing in the product
|
|
2858
|
+
* reads it. */
|
|
2638
2859
|
observations() {
|
|
2639
2860
|
return { presentation: this.submitted, photographs: this.photographs };
|
|
2640
2861
|
}
|
|
@@ -2642,7 +2863,6 @@ export class BlenderRuntimeView {
|
|
|
2642
2863
|
private clear() {
|
|
2643
2864
|
this.skeletons.dispose();
|
|
2644
2865
|
this.skinBinds = new WeakMap();
|
|
2645
|
-
this.worldVisibility.dispose();
|
|
2646
2866
|
this.objectInfoMaterials.clear();
|
|
2647
2867
|
this.materialRanges.clear();
|
|
2648
2868
|
this.motionGeometry.clear();
|
|
@@ -2691,6 +2911,8 @@ export class BlenderRuntimeView {
|
|
|
2691
2911
|
this.extrasOverlay.dispose();
|
|
2692
2912
|
for (const material of this.workbenchMaterials.values()) material.dispose();
|
|
2693
2913
|
this.workbenchMaterials.clear();
|
|
2914
|
+
this.outline.dispose();
|
|
2915
|
+
this.visibleReceivers.dispose();
|
|
2694
2916
|
this.frame = null;
|
|
2695
2917
|
this.retiredSessions.clear();
|
|
2696
2918
|
this.submittedMeshes.clear();
|