@volter/blender-engine 0.1.148 → 0.1.150
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 +22 -5
- 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 +66 -129
- package/browser/blender-engine.mts +20 -186
- package/browser/blender-wali-engine.mts +37 -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 +3 -0
- package/browser/protocol.ts +130 -43
- package/browser/pull-job.mts +2 -10
- package/browser/rna.ts +190 -73
- package/browser/runtime.ts +271 -106
- package/browser/session-addons.mts +33 -0
- package/browser/session-frame.mts +5 -24
- package/browser/session.py +1389 -1446
- 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 +492 -306
- 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 +195 -180
- 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 {
|
|
@@ -1101,24 +1185,9 @@ export class BlenderRuntimeView {
|
|
|
1101
1185
|
}
|
|
1102
1186
|
|
|
1103
1187
|
stageMode(): string | null {
|
|
1104
|
-
return this.
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
/** PLAY IS A MODE OF THE STAGE: while the document plays, the keys are the player's, and the
|
|
1108
|
-
* mode this context publishes is what the frame's key rules stand down on
|
|
1109
|
-
* (`scripts/workbench/generate-keymaps.mjs`, `whenFor`). Blender's own mode returns on stop. */
|
|
1110
|
-
private playing = false;
|
|
1111
|
-
|
|
1112
|
-
setPlaying(playing: boolean): void {
|
|
1113
|
-
this.playing = playing;
|
|
1188
|
+
return this.mode;
|
|
1114
1189
|
}
|
|
1115
1190
|
|
|
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
1191
|
/**
|
|
1123
1192
|
* WHAT THE LAST FRAME SAYS ABOUT ANIMATION, for a reader that plays clips on `skeletons`: the
|
|
1124
1193
|
* scene's current frame and clock, every action's revision, each armature's assigned action, the
|
|
@@ -1136,6 +1205,9 @@ export class BlenderRuntimeView {
|
|
|
1136
1205
|
* pose), its action and its NLA stack. */
|
|
1137
1206
|
armatures: Readonly<Record<string, BlenderArmature>>;
|
|
1138
1207
|
skinArmature(object: string): string | null;
|
|
1208
|
+
/** The scene's camera, every camera's settings and placement, as one string: the movie, which
|
|
1209
|
+
* carries them, is read again when it changes (switching the scene camera, an unkeyed lens). */
|
|
1210
|
+
cameras: string;
|
|
1139
1211
|
} {
|
|
1140
1212
|
const frame = this.frame;
|
|
1141
1213
|
return {
|
|
@@ -1145,6 +1217,11 @@ export class BlenderRuntimeView {
|
|
|
1145
1217
|
active: frame?.active ?? null,
|
|
1146
1218
|
actions: frame?.actions ?? {},
|
|
1147
1219
|
armatures: frame?.armatures ?? {},
|
|
1220
|
+
cameras: JSON.stringify([
|
|
1221
|
+
frame?.camera_view?.scene_camera ?? null,
|
|
1222
|
+
frame?.cameras ?? {},
|
|
1223
|
+
(frame?.objects ?? []).filter((o) => o.type === 'CAMERA').map((o) => [o.name, o.matrix]),
|
|
1224
|
+
]),
|
|
1148
1225
|
skinArmature: (name) => {
|
|
1149
1226
|
const object = this.objectForBlenderName(name) as THREE.Mesh | null;
|
|
1150
1227
|
const skin = object?.geometry?.userData['blenderSkin'] as { armature: string } | undefined;
|
|
@@ -1153,6 +1230,12 @@ export class BlenderRuntimeView {
|
|
|
1153
1230
|
};
|
|
1154
1231
|
}
|
|
1155
1232
|
|
|
1233
|
+
/** The inverse: the presented object for a Blender datablock NAME, or null
|
|
1234
|
+
* when this tab is not showing one (a camera, an excluded collection's
|
|
1235
|
+
* member, an object type the presenter draws nothing for). The OUTLINER
|
|
1236
|
+
* needs it — its rows come from the engine by name, and every one that our
|
|
1237
|
+
* viewport can select has to find its way back to a three object through
|
|
1238
|
+
* the frame's own table, never through `object.name`. */
|
|
1156
1239
|
objectForBlenderName(name: string): THREE.Object3D | null {
|
|
1157
1240
|
const entry = this.frame?.objects.find((o) => o.name === name);
|
|
1158
1241
|
return entry === undefined ? null : (this.objects.get(entry.id) ?? null);
|
|
@@ -1162,16 +1245,8 @@ export class BlenderRuntimeView {
|
|
|
1162
1245
|
* Put `next` where `previous` stood — same parent, same id in the object
|
|
1163
1246
|
* table, same place among its siblings' children.
|
|
1164
1247
|
*
|
|
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.
|
|
1248
|
+
* Frame application and skeleton promotion use this door so replacements
|
|
1249
|
+
* retain the frame's object identity and the next frame can reuse them.
|
|
1175
1250
|
*/
|
|
1176
1251
|
replacePresentedObject(previous: THREE.Object3D, next: THREE.Object3D): void {
|
|
1177
1252
|
for (const [id, object] of this.objects)
|
|
@@ -1194,14 +1269,9 @@ export class BlenderRuntimeView {
|
|
|
1194
1269
|
this.instances.sync();
|
|
1195
1270
|
};
|
|
1196
1271
|
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.
|
|
1272
|
+
// Map Blender Z-up into three.js Y-up with the exact signed permutation (x, y, z) → (x, z, -y).
|
|
1273
|
+
// Camera poses transformed through this matrix and its inverse round-trip without a
|
|
1274
|
+
// floating-point tolerance.
|
|
1205
1275
|
this.root.matrixAutoUpdate = false;
|
|
1206
1276
|
this.root.matrix.set(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1);
|
|
1207
1277
|
this.root.updateMatrixWorld(true);
|
|
@@ -1232,16 +1302,6 @@ export class BlenderRuntimeView {
|
|
|
1232
1302
|
this.emptyExtrasRoot.name = 'BlenderEmptyExtras';
|
|
1233
1303
|
}
|
|
1234
1304
|
|
|
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
1305
|
/**
|
|
1246
1306
|
* BLENDER'S SOLID-MODE STUDIO, for the document to hand to its stage as
|
|
1247
1307
|
* `dressing.viewLocked`.
|
|
@@ -1257,6 +1317,15 @@ export class BlenderRuntimeView {
|
|
|
1257
1317
|
return this.lighting.group;
|
|
1258
1318
|
}
|
|
1259
1319
|
|
|
1320
|
+
/**
|
|
1321
|
+
* THE OVERLAY GROUPS, for the document to hand to its stage.
|
|
1322
|
+
*
|
|
1323
|
+
* `kind` is the `HelperVisibility` member whose checkbox owns it: `skeletons`
|
|
1324
|
+
* is the Helpers menu's Skeletons row, which is Blender's viewport
|
|
1325
|
+
* overlay "Bones" checkbox (`View3DOverlay.show_bones`,
|
|
1326
|
+
* `rna_space.cc:5125-5129`; `space_view3d.py:7161`) under this editor's own
|
|
1327
|
+
* noun; `weights` controls the vertex-group weight display.
|
|
1328
|
+
*/
|
|
1260
1329
|
overlayGroups(): readonly { kind: string; object: THREE.Object3D }[] {
|
|
1261
1330
|
return [
|
|
1262
1331
|
{ kind: 'skeletons', object: this.armatureRoot },
|
|
@@ -1287,6 +1356,7 @@ export class BlenderRuntimeView {
|
|
|
1287
1356
|
return placement;
|
|
1288
1357
|
}
|
|
1289
1358
|
|
|
1359
|
+
private heldSceneLighting: { world: boolean; lights: boolean } | undefined;
|
|
1290
1360
|
/**
|
|
1291
1361
|
* BLENDER'S RENDERED SHADING: the viewport held in the lighting a render photographs with
|
|
1292
1362
|
* ({@link setRendered}) — the scene's own lights, its World behind and around the model,
|
|
@@ -1296,7 +1366,6 @@ export class BlenderRuntimeView {
|
|
|
1296
1366
|
* keeps viewport visibility independently of its shading and lighting choices.
|
|
1297
1367
|
* A render taken meanwhile ends back in this state rather than in modeling.
|
|
1298
1368
|
*/
|
|
1299
|
-
private heldSceneLighting: { world: boolean; lights: boolean } | undefined;
|
|
1300
1369
|
holdRendered(drawCamera: (() => THREE.Camera) | null, sceneLighting?: { world: boolean; lights: boolean }, visibility: 'viewport' | 'render' = 'viewport'): void {
|
|
1301
1370
|
if (drawCamera === this.heldRendered && sceneLighting?.world === this.heldSceneLighting?.world && sceneLighting?.lights === this.heldSceneLighting?.lights && visibility === this.heldVisibility) return;
|
|
1302
1371
|
this.heldRendered = drawCamera;
|
|
@@ -1345,12 +1414,31 @@ export class BlenderRuntimeView {
|
|
|
1345
1414
|
}
|
|
1346
1415
|
|
|
1347
1416
|
private report(work: Promise<void>): void {
|
|
1417
|
+
this.renderedWork = work.then(() => undefined, () => undefined);
|
|
1348
1418
|
void work.then(presenterChanged, (error: unknown) =>
|
|
1349
1419
|
// biome-ignore lint/suspicious/noConsole: the editor console captures console.error session-wide; this is the report channel.
|
|
1350
1420
|
console.error(`Rendered shading could not be applied: ${error instanceof Error ? error.message : String(error)}`),
|
|
1351
1421
|
);
|
|
1352
1422
|
}
|
|
1353
1423
|
|
|
1424
|
+
/** The Rendered shading the viewport was last put in, still settling (`applyRendered`). */
|
|
1425
|
+
private renderedWork: Promise<void> = Promise.resolve();
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* WHAT THE FRAME ON SCREEN IS STILL WAITING ON before it draws as it will: Rendered shading's
|
|
1429
|
+
* settling (area-light tables, the World's sky) and every image's decode. A photograph through
|
|
1430
|
+
* {@link setRendered} waits for the same things; a view presented on the viewport waits here
|
|
1431
|
+
* (the model document's preparation), so a capture right after a model opens or Rendered is
|
|
1432
|
+
* entered does not catch any of them half-way. A graph program still compiling is the draw's
|
|
1433
|
+
* own business (`photographGraphs`).
|
|
1434
|
+
*/
|
|
1435
|
+
async drawReady(): Promise<void> {
|
|
1436
|
+
await this.renderedWork;
|
|
1437
|
+
await lightingReady();
|
|
1438
|
+
await Promise.allSettled([...this.textures.values()].map((held) => held.ready));
|
|
1439
|
+
await this.world.ready().catch(() => undefined);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1354
1442
|
/**
|
|
1355
1443
|
* BLENDER'S SOLID SHADING ON OR OFF: while on, every presented surface is drawn by Blender's
|
|
1356
1444
|
* Solid function over its material's viewport display; a render photograph, Material Preview
|
|
@@ -1363,6 +1451,29 @@ export class BlenderRuntimeView {
|
|
|
1363
1451
|
presenterChanged();
|
|
1364
1452
|
}
|
|
1365
1453
|
|
|
1454
|
+
/**
|
|
1455
|
+
* THE AUTHORED MATERIALS, whatever the viewport shows: `run` sees every presented mesh dressed in the
|
|
1456
|
+
* materials its object's slots name (what Material Preview and Rendered show), and each gets back what
|
|
1457
|
+
* it wore after. An export writes the model's own materials in Solid shading too (`blender-export-gltf`).
|
|
1458
|
+
*/
|
|
1459
|
+
async withAuthoredMaterials<T>(run: () => Promise<T>): Promise<T> {
|
|
1460
|
+
const worn = new Map<THREE.Mesh, THREE.Material | THREE.Material[]>();
|
|
1461
|
+
for (const obj of this.frame?.objects ?? []) {
|
|
1462
|
+
if (obj.mesh === null || obj.volume) continue;
|
|
1463
|
+
const mesh = this.objects.get(obj.id) as THREE.Mesh | undefined;
|
|
1464
|
+
if (!mesh?.isMesh) continue;
|
|
1465
|
+
const slots: readonly (string | null)[] = obj.materials.length ? obj.materials : [null];
|
|
1466
|
+
const authored = slots.map((id) => (id === null ? this.fallback : (this.materials.get(id) ?? this.fallback)));
|
|
1467
|
+
worn.set(mesh, mesh.material);
|
|
1468
|
+
mesh.material = obj.materials.length ? authored : authored[0]!;
|
|
1469
|
+
}
|
|
1470
|
+
try {
|
|
1471
|
+
return await run();
|
|
1472
|
+
} finally {
|
|
1473
|
+
for (const [mesh, material] of worn) mesh.material = material;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1366
1477
|
/** Each surface's material for the state: authored, or Solid's, derived from the frame every
|
|
1367
1478
|
* time so a replaced node (the skin's) is never left wearing the other. */
|
|
1368
1479
|
private applyWorkbench(): void {
|
|
@@ -1376,7 +1487,8 @@ export class BlenderRuntimeView {
|
|
|
1376
1487
|
const slots: readonly (string | null)[] = obj.materials.length ? obj.materials : [null];
|
|
1377
1488
|
const shown = slots.map((id) => {
|
|
1378
1489
|
const authored = id === null ? this.fallback : (this.materials.get(id) ?? this.fallback);
|
|
1379
|
-
|
|
1490
|
+
// Solid ignores a material's Backface Culling (the 3D View's own option governs it, off by default).
|
|
1491
|
+
return solid ? this.workbenchFor(id, THREE.DoubleSide, used) : this.objectInfoMaterials.get(authored,mesh);
|
|
1380
1492
|
});
|
|
1381
1493
|
mesh.material = obj.materials.length ? shown : shown[0]!;
|
|
1382
1494
|
prepareGraphGeometry(mesh);
|
|
@@ -1416,7 +1528,9 @@ export class BlenderRuntimeView {
|
|
|
1416
1528
|
|
|
1417
1529
|
/** Before the renderer uploads attributes/builds its queues, for this area's
|
|
1418
1530
|
* actual camera. Transparent instance runs are camera-order dependent. */
|
|
1419
|
-
prepareDraw(camera: THREE.Camera, options?:
|
|
1531
|
+
prepareDraw(camera: THREE.Camera, options?: BlenderDrawOptions): () => void {
|
|
1532
|
+
const outline = this.outlineColor(options);
|
|
1533
|
+
photographGraphs(options?.photograph === true);
|
|
1420
1534
|
try {
|
|
1421
1535
|
if (!this.workbench || this.rendered) {
|
|
1422
1536
|
if(this.objectInfoMaterials.needsRemap()) this.applyWorkbench();
|
|
@@ -1425,19 +1539,11 @@ export class BlenderRuntimeView {
|
|
|
1425
1539
|
const start = performance.now();
|
|
1426
1540
|
this.instances.prepareDraw(camera);
|
|
1427
1541
|
this.root.updateMatrixWorld(true);
|
|
1542
|
+
this.skeletons.fitBounds();
|
|
1428
1543
|
// The script/navigation has just posed the camera and the detached
|
|
1429
1544
|
// objects. Fit shadows for that draw, rather than the camera's pose
|
|
1430
1545
|
// 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
|
-
}
|
|
1546
|
+
if (this.rendered) this.applyShadows(true, camera, options?.renderer, true);
|
|
1441
1547
|
const opaqueDone = performance.now();
|
|
1442
1548
|
const motion = this.motionGeometry.prepare(camera, !this.rendered && (this.mode ?? this.frame?.mode) === 'OBJECT' &&
|
|
1443
1549
|
options?.interactive === true, options?.height ?? 0);
|
|
@@ -1455,6 +1561,7 @@ export class BlenderRuntimeView {
|
|
|
1455
1561
|
this.drawPreparation.last = timing;
|
|
1456
1562
|
if (motion) this.drawPreparation.navigation = timing;
|
|
1457
1563
|
} catch (error) {
|
|
1564
|
+
photographGraphs(false);
|
|
1458
1565
|
this.materialRanges.finish();
|
|
1459
1566
|
this.motionGeometry.finish();
|
|
1460
1567
|
this.instances.finishDraw();
|
|
@@ -1462,30 +1569,108 @@ export class BlenderRuntimeView {
|
|
|
1462
1569
|
throw error;
|
|
1463
1570
|
}
|
|
1464
1571
|
return () => {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1572
|
+
try {
|
|
1573
|
+
// Over the frame as it was just drawn: its draw geometry and batches still stand.
|
|
1574
|
+
if (outline && options?.renderer) this.drawOutline(options.renderer, camera, outline, options.pixelScale ?? 1);
|
|
1575
|
+
} finally {
|
|
1576
|
+
photographGraphs(false);
|
|
1577
|
+
this.materialRanges.finish();
|
|
1578
|
+
this.motionGeometry.finish();
|
|
1579
|
+
this.instances.finishDraw();
|
|
1580
|
+
this.transparentInstances.finishDraw();
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/** Solid's object outline colour for this draw, or null when it draws none: Blender's Solid
|
|
1586
|
+
* with the file's `show_object_outline` on, and a renderer to draw it with. */
|
|
1587
|
+
private outlineColor(options?: BlenderDrawOptions): readonly [number, number, number] | null {
|
|
1588
|
+
const saved = this.frame?.view;
|
|
1589
|
+
if (!options?.renderer || options.drawMode !== 'solid' || !this.workbench || this.rendered) return null;
|
|
1590
|
+
if (!saved?.show_object_outline) return null;
|
|
1591
|
+
return saved.object_outline_color ?? [0, 0, 0];
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
private drawOutline(renderer: THREE.WebGLRenderer, camera: THREE.Camera, color: readonly [number, number, number], pixelScale: number): void {
|
|
1595
|
+
let scene: THREE.Object3D | null = this.root;
|
|
1596
|
+
while (scene && !(scene as THREE.Scene).isScene) scene = scene.parent;
|
|
1597
|
+
if (!scene) return;
|
|
1598
|
+
const presented = new Set(this.objects.values());
|
|
1599
|
+
// The presenter's own draws (batches) stand in for some objects; the id pass draws those
|
|
1600
|
+
// objects themselves, each with its own id, as Blender's id buffer holds them.
|
|
1601
|
+
const internal = this.root.children.filter((child) => (child as THREE.Mesh).isMesh && !presented.has(child));
|
|
1602
|
+
this.outline.draw(renderer, scene as THREE.Scene, camera, this.objects.values(),
|
|
1603
|
+
(object) => !this.hiddenSurfaceLayers.has(object), internal, color, pixelScale);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/**
|
|
1607
|
+
* HOW EACH SHADING TYPE IS ANTI-ALIASED, as Blender's viewport accumulates it
|
|
1608
|
+
* (`blender-viewport-sampling.ts` holds both models): a photograph draws every sample at once,
|
|
1609
|
+
* and the stage's live view one per redraw while nothing changes, as Blender's viewport does.
|
|
1610
|
+
* - SOLID: Workbench's accumulation at the Preferences' Viewport Anti-Aliasing
|
|
1611
|
+
* (`workbench_state.cc`: `U.viewport_aa` whatever the engine), whose factory value, 8, the tab
|
|
1612
|
+
* assumes: it holds no Blender preferences. Merged over its backdrop as the display shader
|
|
1613
|
+
* merges the overlay.
|
|
1614
|
+
* - RENDERED: Workbench at the scene's `display.viewport_aa` when Workbench is the engine ('OFF'
|
|
1615
|
+
* one centred sample, unfiltered: no anti-aliasing at all; 'FXAA' its one centred sample, the
|
|
1616
|
+
* SMAA Workbench lays over it not reproduced); EEVEE's film otherwise, at its viewport
|
|
1617
|
+
* `taa_samples` and `filter_size`.
|
|
1618
|
+
* - WIREFRAME: none here. Blender's wires are overlay lines smoothed by their own pass, not
|
|
1619
|
+
* accumulated, so a wireframe photograph keeps the stage's own supersampling and the live
|
|
1620
|
+
* view its MSAA.
|
|
1621
|
+
* - MATERIAL PREVIEW: EEVEE's film, whatever the engine.
|
|
1622
|
+
* Null before a frame.
|
|
1623
|
+
*/
|
|
1624
|
+
photographSampling(): Partial<Record<'solid' | 'wireframe' | 'preview' | 'rendered', BlenderPhotographSampling>> | null {
|
|
1625
|
+
const sampling = this.frame?.sampling;
|
|
1626
|
+
if (!sampling) return null;
|
|
1627
|
+
// The same answer for the same settings: the stage keeps its live accumulation while the
|
|
1628
|
+
// sampling it was begun by is the one it is handed (every Blender frame hands it again).
|
|
1629
|
+
const key = `${sampling.engine}|${sampling.viewport_aa}|${sampling.taa_samples}|${sampling.filter_size}`;
|
|
1630
|
+
if (this.#sampling?.key === key) return this.#sampling.value;
|
|
1631
|
+
const film = eeveeSampling(sampling.taa_samples, sampling.filter_size);
|
|
1632
|
+
const sceneAa = sampling.viewport_aa === 'OFF' ? 0 : sampling.viewport_aa === 'FXAA' ? 1 : Number.parseInt(sampling.viewport_aa, 10);
|
|
1633
|
+
const rendered = sampling.engine === 'BLENDER_WORKBENCH'
|
|
1634
|
+
? workbenchSampling(Number.isFinite(sceneAa) ? sceneAa : 1, false) : film;
|
|
1635
|
+
const value = { solid: workbenchSampling(PREFERENCES_VIEWPORT_AA, true), rendered, preview: film };
|
|
1636
|
+
this.#sampling = { key, value };
|
|
1637
|
+
return value;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
#sampling: { readonly key: string; readonly value: Partial<Record<'solid' | 'wireframe' | 'preview' | 'rendered', BlenderPhotographSampling>> } | null = null;
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* THE SCENE'S COLOUR MANAGEMENT for the frame on screen (`Scene.view_settings`, exposure as a
|
|
1644
|
+
* multiplier, and the display device), as `session.py` sends it with every present: Rendered and
|
|
1645
|
+
* Material Preview are drawn through it. Null before a frame, or from an engine that does not
|
|
1646
|
+
* send it.
|
|
1647
|
+
*/
|
|
1648
|
+
displaySettings(): (BlenderDisplaySettings & { readonly device: string }) | null {
|
|
1649
|
+
const display = this.frame?.display;
|
|
1650
|
+
if (!display) return null;
|
|
1651
|
+
return {
|
|
1652
|
+
transform: display.view_transform,
|
|
1653
|
+
look: display.look,
|
|
1654
|
+
exposure: 2 ** display.exposure,
|
|
1655
|
+
gamma: display.gamma,
|
|
1656
|
+
device: display.display_device,
|
|
1469
1657
|
};
|
|
1470
1658
|
}
|
|
1471
1659
|
|
|
1472
1660
|
private workbenchFor(id: string | null, side: THREE.Side, used: Set<string>): THREE.Material {
|
|
1473
1661
|
const display = (id !== null ? this.frame?.materials[id]?.viewport : undefined) ?? DEFAULT_VIEWPORT_DISPLAY;
|
|
1474
|
-
|
|
1662
|
+
// Solid's Specular Lighting, as the file's 3D View holds it (on in a factory file).
|
|
1663
|
+
const specular = this.frame?.view?.show_specular_highlight ?? true;
|
|
1664
|
+
const key = `${display.color.join(',')}|${display.roughness}|${display.metallic}|${side}|${specular}`;
|
|
1475
1665
|
used.add(key);
|
|
1476
1666
|
let material = this.workbenchMaterials.get(key);
|
|
1477
1667
|
if (!material) {
|
|
1478
|
-
material = workbenchMaterial(display, side);
|
|
1668
|
+
material = workbenchMaterial(display, side, specular);
|
|
1479
1669
|
this.workbenchMaterials.set(key, material);
|
|
1480
1670
|
}
|
|
1481
1671
|
return material;
|
|
1482
1672
|
}
|
|
1483
1673
|
|
|
1484
|
-
/** Whether the viewport is held in render lighting (Blender's Rendered shading). */
|
|
1485
|
-
renderedHeld(): boolean {
|
|
1486
|
-
return this.heldRendered !== null;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
1674
|
/**
|
|
1490
1675
|
* Light the model the way a RENDER is lit — by the scene's own lights —
|
|
1491
1676
|
* rather than the way modeling is, by the studio key. `bpy.ops.render.render()`
|
|
@@ -1527,12 +1712,7 @@ export class BlenderRuntimeView {
|
|
|
1527
1712
|
this.applyVisibility();
|
|
1528
1713
|
this.applyWorkbench();
|
|
1529
1714
|
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.
|
|
1715
|
+
// Await preparation after apply registers its sky work, so ready observes every pending texture.
|
|
1536
1716
|
if (rendered) {
|
|
1537
1717
|
await lightingReady();
|
|
1538
1718
|
await Promise.all([...this.textures.values()].map((held) => held.ready));
|
|
@@ -1544,27 +1724,17 @@ export class BlenderRuntimeView {
|
|
|
1544
1724
|
}
|
|
1545
1725
|
|
|
1546
1726
|
/**
|
|
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.
|
|
1727
|
+
* Enable mesh shadows for rendered shading and captures, and fit each
|
|
1728
|
+
* light's shadow camera to the visible receivers and eligible casters.
|
|
1562
1729
|
*/
|
|
1563
|
-
private applyShadows(rendered: boolean, camera?: THREE.Camera): void {
|
|
1564
|
-
|
|
1730
|
+
private applyShadows(rendered: boolean, camera?: THREE.Camera, renderer?: THREE.WebGLRenderer, posed = false): void {
|
|
1731
|
+
// When posed is true, prepareDraw already refreshed world matrices; reuse them.
|
|
1732
|
+
if (!posed) this.root.updateMatrixWorld(true);
|
|
1565
1733
|
this.world.updateSolarDirections(this.root);
|
|
1566
1734
|
const fitted = this.shadowFit;
|
|
1567
|
-
|
|
1735
|
+
// A fit made without a renderer (entering Rendered) is fitted again by the first draw,
|
|
1736
|
+
// which can see what the camera sees.
|
|
1737
|
+
if (rendered && fitted && fitted.camera === camera && (fitted.visible || !renderer || !camera) &&
|
|
1568
1738
|
(!camera || (fitted.cameraMatrix?.equals(camera.matrixWorld) && fitted.projection?.equals(camera.projectionMatrix))) &&
|
|
1569
1739
|
fitted.solarLights.length === this.world.solarLights.length &&
|
|
1570
1740
|
fitted.solarLights.every((light, index) => light === this.world.solarLights[index]) &&
|
|
@@ -1575,6 +1745,7 @@ export class BlenderRuntimeView {
|
|
|
1575
1745
|
})) return;
|
|
1576
1746
|
this.shadowFit = rendered ? {
|
|
1577
1747
|
camera,
|
|
1748
|
+
visible: renderer !== undefined && camera !== undefined,
|
|
1578
1749
|
cameraMatrix: camera?.matrixWorld.clone(),
|
|
1579
1750
|
projection: camera?.projectionMatrix.clone(),
|
|
1580
1751
|
solarLights: [...this.world.solarLights],
|
|
@@ -1603,14 +1774,45 @@ export class BlenderRuntimeView {
|
|
|
1603
1774
|
new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse),
|
|
1604
1775
|
)
|
|
1605
1776
|
: null;
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1777
|
+
const suns = [...this.lights.values(), ...this.world.solarLights]
|
|
1778
|
+
.some((light) => (light as THREE.DirectionalLight).isDirectionalLight && light.castShadow);
|
|
1779
|
+
// WHAT THE CAMERA SEES is what a sun's map covers, as EEVEE tags its pages from the visible
|
|
1780
|
+
// surfaces (`VisibleReceivers`); the frustum's boxes only when no renderer is at hand to look.
|
|
1781
|
+
const seen = suns && renderer && camera ? this.sampleVisibleReceivers(renderer, camera) : [];
|
|
1782
|
+
// Every visible receiver is needed only to fit a sun without a renderer to look; a point or spot
|
|
1783
|
+
// light's bias needs only the farthest one (`farthestVisibleReceiver`).
|
|
1784
|
+
let clipped: THREE.Vector3[] | null = null;
|
|
1785
|
+
const receivers = (): THREE.Vector3[] => seen.length
|
|
1786
|
+
? seen
|
|
1787
|
+
: frustum
|
|
1788
|
+
? (clipped ??= boxes.flatMap((bounds) => visibleShadowReceivers(bounds, frustum)))
|
|
1789
|
+
: [];
|
|
1609
1790
|
for (const light of [...this.lights.values(), ...this.world.solarLights]) {
|
|
1610
1791
|
if ((light as THREE.DirectionalLight).isDirectionalLight && camera)
|
|
1611
|
-
fitModelDirectionalShadow(light as THREE.DirectionalLight, receivers, casters);
|
|
1612
|
-
else
|
|
1792
|
+
fitModelDirectionalShadow(light as THREE.DirectionalLight, light.castShadow ? receivers() : [], casters);
|
|
1793
|
+
else {
|
|
1794
|
+
fitShadow(light, sphere.center, sphere.radius);
|
|
1795
|
+
if ((light as THREE.PointLight).isPointLight || (light as THREE.SpotLight).isSpotLight)
|
|
1796
|
+
fitPerspectiveShadowBias(light as THREE.PointLight | THREE.SpotLight, seen.length || !frustum ? receivers()
|
|
1797
|
+
: light.castShadow ? farthestVisibleReceiver(boxes, frustum, light.getWorldPosition(new THREE.Vector3())) : []);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/** The visible surfaces through `camera`, leaving out what the drawing sees through. */
|
|
1803
|
+
private sampleVisibleReceivers(renderer: THREE.WebGLRenderer, camera: THREE.Camera): THREE.Vector3[] {
|
|
1804
|
+
let scene: THREE.Object3D | null = this.root;
|
|
1805
|
+
while (scene && !(scene as THREE.Scene).isScene) scene = scene.parent;
|
|
1806
|
+
if (!scene) return [];
|
|
1807
|
+
const seeThrough: THREE.Object3D[] = [];
|
|
1808
|
+
for (const object of this.objects.values()) {
|
|
1809
|
+
const mesh = object as THREE.Mesh;
|
|
1810
|
+
if (!mesh.isMesh) continue;
|
|
1811
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
1812
|
+
if (materials.every((material) => material.transparent || (material as THREE.MeshPhysicalMaterial).transmission > 0))
|
|
1813
|
+
seeThrough.push(mesh);
|
|
1613
1814
|
}
|
|
1815
|
+
return this.visibleReceivers.sample(renderer, scene as THREE.Scene, camera, seeThrough);
|
|
1614
1816
|
}
|
|
1615
1817
|
|
|
1616
1818
|
/**
|
|
@@ -1708,6 +1910,8 @@ export class BlenderRuntimeView {
|
|
|
1708
1910
|
});
|
|
1709
1911
|
}
|
|
1710
1912
|
|
|
1913
|
+
/** Use render visibility for captures and views that request it. Editing
|
|
1914
|
+
* retains viewport visibility independently of its shading and lighting. */
|
|
1711
1915
|
private applyVisibility(): void {
|
|
1712
1916
|
// Restore canonical masks before suppressing an instancer's own surface.
|
|
1713
1917
|
// Its geometry can be hidden while its placed children remain visible.
|
|
@@ -1765,10 +1969,9 @@ export class BlenderRuntimeView {
|
|
|
1765
1969
|
const data = exportedMeshSchema.parse(part.piece);
|
|
1766
1970
|
if (staged.meshes.has(part.mesh)) throw new Error('Duplicate Blender mesh piece');
|
|
1767
1971
|
const signature = `revision:${data.revision}`;
|
|
1768
|
-
const geometry = geometryFromDrawArrays(drawArraysFromColumns({
|
|
1972
|
+
const geometry = drawnOrRefused(() => withFrameSkin(geometryFromDrawArrays(drawArraysFromColumns({
|
|
1769
1973
|
...data.columns, attributes: data.attributes as never, activeUv: data.activeUv, renderUv: data.renderUv,
|
|
1770
|
-
}, signature));
|
|
1771
|
-
if (data.skin) skinGeometry(geometry, data.skin);
|
|
1974
|
+
}, signature)), data.skin));
|
|
1772
1975
|
staged.meshes.set(part.mesh, { signature, geometry });
|
|
1773
1976
|
} else if (part.image !== undefined) {
|
|
1774
1977
|
if (staged.images.has(part.image)) throw new Error('Duplicate Blender image piece');
|
|
@@ -1856,7 +2059,7 @@ export class BlenderRuntimeView {
|
|
|
1856
2059
|
// BufferGeometry stands. If it is not resident the worker's record
|
|
1857
2060
|
// is ahead of this presenter — a page reloaded under a still-running
|
|
1858
2061
|
// session — and saying so by name is what makes it re-send the frame
|
|
1859
|
-
// in full
|
|
2062
|
+
// in full.
|
|
1860
2063
|
// AND AT THAT REVISION: the frame names every mesh with the revision the engine holds
|
|
1861
2064
|
// it at, so it is a whole manifest of what this presenter should hold, and a resident
|
|
1862
2065
|
// geometry at another revision has drifted from the engine (a present the session
|
|
@@ -1890,7 +2093,7 @@ export class BlenderRuntimeView {
|
|
|
1890
2093
|
continue;
|
|
1891
2094
|
prepared.set(id, {
|
|
1892
2095
|
signature,
|
|
1893
|
-
geometry: exported
|
|
2096
|
+
geometry: drawnOrRefused(() => exported
|
|
1894
2097
|
? withFrameSkin(geometryFromDrawArrays(
|
|
1895
2098
|
drawArraysFromColumns(
|
|
1896
2099
|
{
|
|
@@ -1904,7 +2107,7 @@ export class BlenderRuntimeView {
|
|
|
1904
2107
|
), data.skin)
|
|
1905
2108
|
: drawn
|
|
1906
2109
|
? withDrawnSkin(geometryFromDrawArrays(data), data.skin)
|
|
1907
|
-
: drawRuntimeGeometry(data),
|
|
2110
|
+
: drawRuntimeGeometry(data)),
|
|
1908
2111
|
});
|
|
1909
2112
|
}
|
|
1910
2113
|
} catch (error) {
|
|
@@ -1939,7 +2142,7 @@ export class BlenderRuntimeView {
|
|
|
1939
2142
|
}
|
|
1940
2143
|
// THE PICTURES THE FRAME BROUGHT, before any material can ask for one. An
|
|
1941
2144
|
// entry is here only because this session has not sent these bytes at this
|
|
1942
|
-
// revision yet (`
|
|
2145
|
+
// revision yet (`session.py`'s image-carry record); everything else the
|
|
1943
2146
|
// materials name is already resident, which is the whole point of the
|
|
1944
2147
|
// revision.
|
|
1945
2148
|
for (const [name, data] of Object.entries(next.images)) {
|
|
@@ -1999,27 +2202,22 @@ export class BlenderRuntimeView {
|
|
|
1999
2202
|
});
|
|
2000
2203
|
}
|
|
2001
2204
|
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.
|
|
2205
|
+
// Render both surface faces, as Cycles does. Cast shadows from both faces unless the material
|
|
2206
|
+
// enables shadow backface culling. Fit the sun map to visible receivers and scale its normal
|
|
2207
|
+
// offset by the map’s texel size.
|
|
2007
2208
|
const material = this.materials.get(id) ??
|
|
2008
|
-
new THREE.MeshPhysicalMaterial({ side: THREE.DoubleSide, shadowSide: THREE.
|
|
2209
|
+
new THREE.MeshPhysicalMaterial({ side: THREE.DoubleSide, shadowSide: THREE.DoubleSide });
|
|
2210
|
+
material.shadowSide = data.backface_culling_shadow ? THREE.FrontSide : THREE.DoubleSide;
|
|
2211
|
+
// Match Blender’s face passes: cull back faces when declared, and draw blended surfaces in one pass with Transparency Overlap enabled.
|
|
2212
|
+
const side = data.backface_culling ? THREE.FrontSide : THREE.DoubleSide;
|
|
2213
|
+
const singlePass = data.transparency_overlap !== false;
|
|
2214
|
+
if (material.side !== side || material.forceSinglePass !== singlePass) {
|
|
2215
|
+
material.side = side;
|
|
2216
|
+
material.forceSinglePass = singlePass;
|
|
2217
|
+
material.needsUpdate = true;
|
|
2218
|
+
}
|
|
2009
2219
|
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.
|
|
2220
|
+
// 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
2221
|
const tint = data.texture?.tint;
|
|
2024
2222
|
if (data.texture !== undefined) {
|
|
2025
2223
|
material.color.setRGB(
|
|
@@ -2117,10 +2315,7 @@ export class BlenderRuntimeView {
|
|
|
2117
2315
|
material.needsUpdate = true;
|
|
2118
2316
|
}
|
|
2119
2317
|
}
|
|
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.
|
|
2318
|
+
// Base Color images are sRGB; declare that encoding so three linearizes their texels.
|
|
2124
2319
|
const wanted = data.texture?.image.name ?? null;
|
|
2125
2320
|
if (wanted === null) {
|
|
2126
2321
|
if (this.textureNames.has(id)) {
|
|
@@ -2304,17 +2499,13 @@ export class BlenderRuntimeView {
|
|
|
2304
2499
|
if (bound && rig) this.skinBinds.set(bound, { geometry: bound.geometry, rig });
|
|
2305
2500
|
}
|
|
2306
2501
|
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.
|
|
2502
|
+
// Keep frame identity and object metadata after presentation; BufferGeometry owns the geometry bytes and per-mesh signatures support reuse checks.
|
|
2312
2503
|
this.frame = { ...next, meshes: {}, volumes: {}, images: {} };
|
|
2313
2504
|
this.worldKey = JSON.stringify(next.world ?? null);
|
|
2314
2505
|
// THE OVERLAYS, after the graph stands: the weight drawing is laid over
|
|
2315
2506
|
// the presented mesh's own geometry and the bones over the armature
|
|
2316
2507
|
// OBJECT's Blender matrix, so both need this frame's objects in place.
|
|
2317
|
-
const overlayWarnings = this.applyOverlays(next);
|
|
2508
|
+
const overlayWarnings = [...this.applyOverlays(next), ...this.nameRefusals(next.objects)];
|
|
2318
2509
|
if (overlayWarnings.length)
|
|
2319
2510
|
this.frame = { ...this.frame, warnings: [...this.frame.warnings, ...overlayWarnings] };
|
|
2320
2511
|
// Visibility is read off the frame, so it is applied once the frame stands.
|
|
@@ -2338,6 +2529,36 @@ export class BlenderRuntimeView {
|
|
|
2338
2529
|
return { ...this.inspect(), held };
|
|
2339
2530
|
}
|
|
2340
2531
|
|
|
2532
|
+
/** The meshes this presenter holds but could not draw, by mesh id, with the reason. */
|
|
2533
|
+
refusedMeshes(): Record<string, string> {
|
|
2534
|
+
const out: Record<string, string> = {};
|
|
2535
|
+
for (const [id, entry] of this.meshes) {
|
|
2536
|
+
const reason = entry.geometry.userData[REFUSED_GEOMETRY];
|
|
2537
|
+
if (typeof reason === 'string') out[id] = reason;
|
|
2538
|
+
}
|
|
2539
|
+
return out;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
/** Every held mesh that could not be drawn, named with the objects that show it: a frame
|
|
2543
|
+
* warning each frame, and on the editor's console once per mesh, revision and reason. */
|
|
2544
|
+
private nameRefusals(objects: readonly { name: string; mesh: string | null }[]): string[] {
|
|
2545
|
+
const said: string[] = [];
|
|
2546
|
+
for (const [id, reason] of Object.entries(this.refusedMeshes())) {
|
|
2547
|
+
const users = objects.filter((obj) => obj.mesh === id).map((obj) => JSON.stringify(obj.name));
|
|
2548
|
+
const shown = users.length > 8 ? `${users.slice(0, 8).join(', ')} and ${users.length - 8} more` : users.join(', ');
|
|
2549
|
+
const line = `The presenter skipped mesh ${JSON.stringify(id)}` +
|
|
2550
|
+
(users.length ? ` (${users.length > 1 ? 'objects' : 'object'} ${shown})` : '') +
|
|
2551
|
+
`, which it cannot draw: ${reason}. The rest of the document presents; the mesh is drawn again once it changes.`;
|
|
2552
|
+
said.push(line);
|
|
2553
|
+
const key = `${id}\u0000${this.meshes.get(id)?.signature}\u0000${reason}`;
|
|
2554
|
+
if (NAMED_REFUSALS.has(key)) continue;
|
|
2555
|
+
NAMED_REFUSALS.add(key);
|
|
2556
|
+
// biome-ignore lint/suspicious/noConsole: the editor console captures console.warn session-wide; this is the report channel.
|
|
2557
|
+
console.warn(`[blender] @@VOLTER-WARN ${line}`);
|
|
2558
|
+
}
|
|
2559
|
+
return said;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2341
2562
|
inspect() {
|
|
2342
2563
|
return {
|
|
2343
2564
|
session: this.frame?.session,
|
|
@@ -2357,31 +2578,23 @@ export class BlenderRuntimeView {
|
|
|
2357
2578
|
}
|
|
2358
2579
|
|
|
2359
2580
|
drawStatistics() {
|
|
2360
|
-
return {enabled: this.drawBatching, preparation: {...this.drawPreparation},
|
|
2581
|
+
return {enabled: this.drawBatching, preparation: {...this.drawPreparation}, materialRanges: this.materialRanges.inspect(), opaque: this.instances.inspect(), transparent: this.transparentInstances.inspect(), motion: this.motionGeometry.inspect()};
|
|
2361
2582
|
}
|
|
2362
2583
|
|
|
2363
2584
|
snapshot() {
|
|
2364
2585
|
return this.frame;
|
|
2365
2586
|
}
|
|
2366
2587
|
|
|
2367
|
-
/**
|
|
2368
|
-
* A revision-owned render presenter, never a clone of the mounted scene.
|
|
2369
|
-
* Copy the resident draw arrays and image bytes synchronously, then rebuild
|
|
2370
|
-
* through the same frame contract. No live objects, callbacks, textures or
|
|
2371
|
-
* disposable geometry cross this boundary; delta-only frames are sufficient.
|
|
2372
|
-
* The caller owns this snapshot and must dispose it on every outcome.
|
|
2373
|
-
*/
|
|
2374
2588
|
/** Everything this view holds, as ONE self-contained frame another view can be built from:
|
|
2375
2589
|
* every mesh's resident geometry and every resident image, with no reference to what a
|
|
2376
2590
|
* receiver already holds. */
|
|
2377
|
-
private fullFrame(
|
|
2591
|
+
private fullFrame(): Frame {
|
|
2378
2592
|
const source = this.frame;
|
|
2379
2593
|
if (!source) throw new Error('Blender capture requires a presented frame');
|
|
2380
2594
|
// Inspection overlays are not render content. In particular weight-paint
|
|
2381
2595
|
// 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 });
|
|
2596
|
+
// needs and a render snapshot deliberately does not copy. A render snapshot draws no bones.
|
|
2597
|
+
const frame: Frame = structuredClone({ ...source, images: {}, armatures: {}, weights: null });
|
|
2385
2598
|
for (const [id, { signature, geometry }] of this.meshes) {
|
|
2386
2599
|
const attribute = (name: string): Float32Array<ArrayBuffer> | null => {
|
|
2387
2600
|
const value = geometry.getAttribute(name);
|
|
@@ -2417,20 +2630,6 @@ export class BlenderRuntimeView {
|
|
|
2417
2630
|
count: group.count,
|
|
2418
2631
|
materialIndex: group.materialIndex ?? 0,
|
|
2419
2632
|
})),
|
|
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
2633
|
};
|
|
2435
2634
|
}
|
|
2436
2635
|
for (const [id, { signature }] of this.volumes)
|
|
@@ -2459,7 +2658,7 @@ export class BlenderRuntimeView {
|
|
|
2459
2658
|
colorspace: held.encoded.colorspace,
|
|
2460
2659
|
};
|
|
2461
2660
|
} else {
|
|
2462
|
-
const data = held.texture.image?.data;
|
|
2661
|
+
const data = (held.texture.image as { data?: unknown } | null | undefined)?.data;
|
|
2463
2662
|
if (!(data instanceof Uint8Array))
|
|
2464
2663
|
throw new Error(`Blender capture image ${name} has no resident raster`);
|
|
2465
2664
|
frame.images[name] = {
|
|
@@ -2503,25 +2702,12 @@ export class BlenderRuntimeView {
|
|
|
2503
2702
|
}
|
|
2504
2703
|
|
|
2505
2704
|
/**
|
|
2506
|
-
* A
|
|
2507
|
-
*
|
|
2508
|
-
*
|
|
2705
|
+
* A revision-owned render presenter, never a clone of the mounted scene.
|
|
2706
|
+
* Copy the resident draw arrays and image bytes synchronously, then rebuild
|
|
2707
|
+
* through the same frame contract. No live objects, callbacks, textures or
|
|
2708
|
+
* disposable geometry cross this boundary; delta-only frames are sufficient.
|
|
2709
|
+
* The caller owns this snapshot and must dispose it on every outcome.
|
|
2509
2710
|
*/
|
|
2510
|
-
detach(): { readonly view: BlenderRuntimeView; dispose(): void } {
|
|
2511
|
-
const detached = new BlenderRuntimeView();
|
|
2512
|
-
if (this.frame) detached.applyFrame(this.fullFrame({ sourceVertices: true }));
|
|
2513
|
-
let disposed = false;
|
|
2514
|
-
return {
|
|
2515
|
-
view: detached,
|
|
2516
|
-
dispose: () => {
|
|
2517
|
-
if (disposed) return;
|
|
2518
|
-
disposed = true;
|
|
2519
|
-
detached.dispose();
|
|
2520
|
-
detached.root.removeFromParent();
|
|
2521
|
-
},
|
|
2522
|
-
};
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
2711
|
captureSnapshot() {
|
|
2526
2712
|
const frame = this.fullFrame();
|
|
2527
2713
|
const source = this.frame!;
|
|
@@ -2556,7 +2742,7 @@ export class BlenderRuntimeView {
|
|
|
2556
2742
|
},
|
|
2557
2743
|
prepareDraw:(renderer:THREE.WebGLRenderer,camera:THREE.Camera)=>{
|
|
2558
2744
|
if(disposed)throw new Error('Blender capture snapshot is disposed');
|
|
2559
|
-
return detached.prepareDraw(camera,{interactive:false,height:renderer.domElement.height,renderer});
|
|
2745
|
+
return detached.prepareDraw(camera,{interactive:false,height:renderer.domElement.height,renderer,photograph:true});
|
|
2560
2746
|
},
|
|
2561
2747
|
dispose,
|
|
2562
2748
|
};
|
|
@@ -2584,7 +2770,7 @@ export class BlenderRuntimeView {
|
|
|
2584
2770
|
},
|
|
2585
2771
|
prepareDraw: (renderer, camera) => {
|
|
2586
2772
|
if (disposed) throw new Error('Blender capture snapshot is disposed');
|
|
2587
|
-
return this.prepareDraw(camera, { interactive: false, height: renderer.domElement.height, renderer });
|
|
2773
|
+
return this.prepareDraw(camera, { interactive: false, height: renderer.domElement.height, renderer, photograph: true });
|
|
2588
2774
|
},
|
|
2589
2775
|
dispose: () => {
|
|
2590
2776
|
if (disposed) return;
|
|
@@ -2632,9 +2818,8 @@ export class BlenderRuntimeView {
|
|
|
2632
2818
|
}
|
|
2633
2819
|
|
|
2634
2820
|
/** 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. */
|
|
2821
|
+
* from outside through the editor's document REPL. Nothing in the product
|
|
2822
|
+
* reads it. */
|
|
2638
2823
|
observations() {
|
|
2639
2824
|
return { presentation: this.submitted, photographs: this.photographs };
|
|
2640
2825
|
}
|
|
@@ -2642,7 +2827,6 @@ export class BlenderRuntimeView {
|
|
|
2642
2827
|
private clear() {
|
|
2643
2828
|
this.skeletons.dispose();
|
|
2644
2829
|
this.skinBinds = new WeakMap();
|
|
2645
|
-
this.worldVisibility.dispose();
|
|
2646
2830
|
this.objectInfoMaterials.clear();
|
|
2647
2831
|
this.materialRanges.clear();
|
|
2648
2832
|
this.motionGeometry.clear();
|
|
@@ -2691,6 +2875,8 @@ export class BlenderRuntimeView {
|
|
|
2691
2875
|
this.extrasOverlay.dispose();
|
|
2692
2876
|
for (const material of this.workbenchMaterials.values()) material.dispose();
|
|
2693
2877
|
this.workbenchMaterials.clear();
|
|
2878
|
+
this.outline.dispose();
|
|
2879
|
+
this.visibleReceivers.dispose();
|
|
2694
2880
|
this.frame = null;
|
|
2695
2881
|
this.retiredSessions.clear();
|
|
2696
2882
|
this.submittedMeshes.clear();
|