@volter/blender-engine 0.1.148 → 0.1.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -0
- package/README.md +4 -11
- package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
- package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
- package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
- package/browser/addons/vrm/README.txt +3 -0
- package/browser/addons/vrm/__init__.py +41 -0
- package/browser/addons/vrm/blender_manifest.toml +16 -0
- package/browser/addons/vrm/common/__init__.py +1 -0
- package/browser/addons/vrm/common/animation.py +58 -0
- package/browser/addons/vrm/common/blender_manifest.py +82 -0
- package/browser/addons/vrm/common/char.py +16 -0
- package/browser/addons/vrm/common/convert.py +287 -0
- package/browser/addons/vrm/common/convert_any.py +48 -0
- package/browser/addons/vrm/common/debug.py +40 -0
- package/browser/addons/vrm/common/deep.py +162 -0
- package/browser/addons/vrm/common/error_dialog.py +274 -0
- package/browser/addons/vrm/common/fs.py +71 -0
- package/browser/addons/vrm/common/gl.py +20 -0
- package/browser/addons/vrm/common/gltf.py +1149 -0
- package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
- package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
- package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
- package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
- package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
- package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
- package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
- package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
- package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
- package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
- package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
- package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
- package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
- package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
- package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
- package/browser/addons/vrm/common/legacy_gltf.py +16 -0
- package/browser/addons/vrm/common/logger.py +84 -0
- package/browser/addons/vrm/common/mtoon0.blend +0 -0
- package/browser/addons/vrm/common/mtoon1.blend +0 -0
- package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
- package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
- package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
- package/browser/addons/vrm/common/ops/__init__.py +10 -0
- package/browser/addons/vrm/common/ops/export_scene.py +67 -0
- package/browser/addons/vrm/common/ops/icyp.py +53 -0
- package/browser/addons/vrm/common/ops/import_scene.py +57 -0
- package/browser/addons/vrm/common/ops/vrm.py +2380 -0
- package/browser/addons/vrm/common/ops/wm.py +167 -0
- package/browser/addons/vrm/common/preferences.py +362 -0
- package/browser/addons/vrm/common/progress.py +87 -0
- package/browser/addons/vrm/common/rotation.py +89 -0
- package/browser/addons/vrm/common/safe_removal.py +46 -0
- package/browser/addons/vrm/common/scene_watcher.py +230 -0
- package/browser/addons/vrm/common/shader.py +2243 -0
- package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
- package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
- package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
- package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
- package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
- package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
- package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
- package/browser/addons/vrm/common/version.py +161 -0
- package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
- package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
- package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
- package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
- package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
- package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
- package/browser/addons/vrm/common/workspace.py +227 -0
- package/browser/addons/vrm/common/writable_context.py +77 -0
- package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
- package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
- package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
- package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
- package/browser/addons/vrm/documentation/en-us/index.md +41 -0
- package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
- package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
- package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
- package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
- package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
- package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
- package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
- package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
- package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
- package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
- package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
- package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
- package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
- package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
- package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
- package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
- package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
- package/browser/addons/vrm/editor/__init__.py +1 -0
- package/browser/addons/vrm/editor/extension.py +425 -0
- package/browser/addons/vrm/editor/extension_accessor.py +75 -0
- package/browser/addons/vrm/editor/handler.py +84 -0
- package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
- package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
- package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
- package/browser/addons/vrm/editor/make_armature.py +709 -0
- package/browser/addons/vrm/editor/menu.py +147 -0
- package/browser/addons/vrm/editor/migration.py +297 -0
- package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
- package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
- package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
- package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
- package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
- package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
- package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
- package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
- package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
- package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
- package/browser/addons/vrm/editor/ops.py +471 -0
- package/browser/addons/vrm/editor/panel.py +315 -0
- package/browser/addons/vrm/editor/property_group.py +888 -0
- package/browser/addons/vrm/editor/search.py +682 -0
- package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
- package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
- package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
- package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
- package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
- package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
- package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
- package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
- package/browser/addons/vrm/editor/subscription.py +123 -0
- package/browser/addons/vrm/editor/t_pose.py +713 -0
- package/browser/addons/vrm/editor/validation.py +1271 -0
- package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
- package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
- package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
- package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
- package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
- package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
- package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
- package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
- package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
- package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
- package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
- package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
- package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
- package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
- package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
- package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
- package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
- package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
- package/browser/addons/vrm/exporter/__init__.py +1 -0
- package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
- package/browser/addons/vrm/exporter/export_scene.py +930 -0
- package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
- package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
- package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
- package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
- package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
- package/browser/addons/vrm/external/__init__.py +1 -0
- package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
- package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
- package/browser/addons/vrm/external/rigify_support.py +1 -0
- package/browser/addons/vrm/importer/__init__.py +1 -0
- package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
- package/browser/addons/vrm/importer/file_handler.py +34 -0
- package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
- package/browser/addons/vrm/importer/import_scene.py +646 -0
- package/browser/addons/vrm/importer/license_validation.py +198 -0
- package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
- package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
- package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
- package/browser/addons/vrm/importer/vrm_diff.py +316 -0
- package/browser/addons/vrm/locale/__init__.py +1 -0
- package/browser/addons/vrm/locale/ja_jp.py +826 -0
- package/browser/addons/vrm/locale/ruff.toml +8 -0
- package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
- package/browser/addons/vrm/locale/zh_hans.py +928 -0
- package/browser/addons/vrm/registration.py +670 -0
- package/browser/addons/vrm.json +18 -0
- package/browser/blender-emscripten-engine.mts +60 -128
- package/browser/blender-engine.mts +20 -186
- package/browser/blender-wali-engine.mts +35 -81
- package/browser/describe-thrown.mts +3 -6
- package/browser/document-chunks.mts +4 -16
- package/browser/gltf_export.py +3161 -0
- package/browser/heap-growth.mts +23 -0
- package/browser/index.ts +2 -0
- package/browser/protocol.ts +119 -35
- package/browser/pull-job.mts +2 -10
- package/browser/rna.ts +190 -73
- package/browser/runtime.ts +197 -82
- package/browser/session-addons.mts +33 -0
- package/browser/session-frame.mts +5 -24
- package/browser/session.py +1338 -1427
- package/browser/three/attach-presenter.ts +11 -39
- package/browser/three/blender-base64.ts +1 -36
- package/browser/three/blender-corner-normals.ts +1 -23
- package/browser/three/blender-display-shaders.json +1 -1
- package/browser/three/blender-display-shaders.lut +0 -0
- package/browser/three/blender-display-transform.ts +2 -1
- package/browser/three/blender-frame-columns.ts +5 -13
- package/browser/three/blender-graph-material.ts +18 -3
- package/browser/three/blender-node-graph.ts +10 -34
- package/browser/three/blender-physical-material.ts +2 -31
- package/browser/three/blender-runtime-armature.ts +7 -60
- package/browser/three/blender-runtime-frame.ts +9 -23
- package/browser/three/blender-runtime-geometry.ts +22 -26
- package/browser/three/blender-runtime-lighting.ts +48 -178
- package/browser/three/blender-runtime-shadows.ts +187 -1
- package/browser/three/blender-runtime-skeleton.ts +53 -1
- package/browser/three/blender-runtime-view.ts +505 -283
- package/browser/three/blender-runtime-weights.ts +9 -72
- package/browser/three/blender-sky.ts +13 -33
- package/browser/three/blender-triangulate.ts +7 -35
- package/browser/three/blender-viewport-sampling.ts +235 -0
- package/browser/three/blender-workbench-material.ts +25 -29
- package/browser/three/blender-workbench-outline.ts +199 -0
- package/browser/three/blender-world-volume.ts +62 -10
- package/browser/three/presenter.ts +10 -26
- package/browser/three/release.ts +5 -21
- package/browser/three/sky-precompute-worker.ts +1 -18
- package/browser/three/sky-worker.ts +5 -23
- package/browser/three/world-field-sampler.ts +12 -26
- package/browser/volter_three.py +9 -105
- package/browser/worker.ts +156 -151
- package/package.json +5 -2
- package/wasm/BUNDLE.json +17 -17
- package/wasm/DEPENDENCY-LICENSES.txt +14 -0
- package/wasm/blender_browser.data.br +0 -0
- package/wasm/blender_browser.js +1 -1
- package/wasm/blender_browser.wasm.br +0 -0
- package/browser/frame-transfer.mts +0 -19
- package/browser/three/agx-base-srgb.lut +0 -0
- package/browser/three/agx-look-medium-high-contrast.lut +0 -0
- package/browser/three/agx-look-punchy.lut +0 -0
- package/browser/three/blender-agx.ts +0 -235
- package/browser/three/blender-display-lut.ts +0 -145
- package/browser/three/blender-filmic.ts +0 -49
- package/browser/three/blender-standard.ts +0 -68
- package/browser/three/blender-world-visibility.ts +0 -278
- package/browser/three/filmic-srgb.lut +0 -0
|
@@ -1,38 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* while it is being built; a RENDER is lit by the scene's own lights, because
|
|
8
|
-
* that is what the script placed them for. `setRendered` switches between
|
|
9
|
-
* them, and a render capture is the only caller that asks for the second.
|
|
10
|
-
*
|
|
11
|
-
* The studio lives here rather than in the standard viewport dressing because
|
|
12
|
-
* it is BLENDER's, not the editor's: the dressing's key is one warm light in
|
|
13
|
-
* the world, and Solid mode's is four in view space with no world light at
|
|
14
|
-
* all. The Model document turns the dressing's key AND its IBL off and hands
|
|
15
|
-
* this group over as its `dressing.viewLocked` instead.
|
|
16
|
-
*
|
|
17
|
-
* UNITS. The frame states Blender's own values — watts, radians, metres — and
|
|
18
|
-
* the conversion to three.js's photometric intensities happens here, derived
|
|
19
|
-
* rather than tuned:
|
|
20
|
-
*
|
|
21
|
-
* - POINT and SPOT. Blender spreads `energy` watts over the whole sphere, so
|
|
22
|
-
* irradiance at distance d is P/(4πd²); three.js gives I/d². Hence I = P/4π.
|
|
23
|
-
* - SUN. `energy` is already an irradiance (W/m²) and a directional light's
|
|
24
|
-
* intensity is the same quantity, so it passes through.
|
|
25
|
-
* - AREA. P watts leave an area A into a hemisphere, so the radiance is
|
|
26
|
-
* P/(Aπ), which is what `RectAreaLight.intensity` means. A DISK or ELLIPSE
|
|
27
|
-
* has no rectangular equivalent, so it becomes the rectangle of EQUAL AREA:
|
|
28
|
-
* the total emission is right and the shape of a soft shadow's edge is not.
|
|
29
|
-
*
|
|
30
|
-
* SHADOWS follow `use_shadow`, the same flag Cycles reads. A shadow MAP is not
|
|
31
|
-
* a traced shadow: its softness comes from filtering a depth buffer rather than
|
|
32
|
-
* from the light's physical size, so `shadow_soft_size` and a sun's `angle`
|
|
33
|
-
* still reach the frame and still change nothing. A RectAreaLight casts none at
|
|
34
|
-
* all — three.js has no shadow for it — and that is left as it is rather than
|
|
35
|
-
* faked with a substitute light the scene never declared.
|
|
2
|
+
* Manage Blender scene lights and the document’s view-locked Solid studio. setRendered selects
|
|
3
|
+
* authored lighting; Solid mode uses the studio provided as dressing.viewLocked. Convert Blender’s
|
|
4
|
+
* energy units to three.js intensity here: point and spot P/(4π), sun irradiance unchanged, and
|
|
5
|
+
* area radiance P/(Aπ). Area shapes use equal-area rectangles; shadow support follows the
|
|
6
|
+
* presentation’s light-specific paths.
|
|
36
7
|
*/
|
|
37
8
|
import * as THREE from 'three';
|
|
38
9
|
import {presenterChanged} from './blender-presenter-change';
|
|
@@ -111,8 +82,10 @@ const areaExtent = (data: LightData): [number, number] => {
|
|
|
111
82
|
return [size, size];
|
|
112
83
|
};
|
|
113
84
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
85
|
+
/** A sun's shadow map, on a side: the World's suns and the scene's own. */
|
|
86
|
+
export const SUN_SHADOW_MAP_SIZE = 2048;
|
|
87
|
+
|
|
88
|
+
/** Build or update the three.js light for one Blender light datablock. */
|
|
116
89
|
export function buildLight(data: LightData, existing: THREE.Light | null): THREE.Light {
|
|
117
90
|
const reuse = <T extends THREE.Light>(
|
|
118
91
|
ctor: new () => T,
|
|
@@ -126,6 +99,14 @@ export function buildLight(data: LightData, existing: THREE.Light | null): THREE
|
|
|
126
99
|
light.intensity = data.energy;
|
|
127
100
|
light.color.setRGB(data.color[0], data.color[1], data.color[2], THREE.LinearSRGBColorSpace);
|
|
128
101
|
light.castShadow = data.use_shadow;
|
|
102
|
+
// The World's sun's map size (`WorldBackground.compose`), not three's 512: a sun's map is
|
|
103
|
+
// fitted over everything the camera sees (`blender-runtime-shadows.ts`). Three allocates the
|
|
104
|
+
// map once, so a size it does not have yet replaces the map.
|
|
105
|
+
if (light.shadow.mapSize.x !== SUN_SHADOW_MAP_SIZE || light.shadow.mapSize.y !== SUN_SHADOW_MAP_SIZE) {
|
|
106
|
+
light.shadow.mapSize.set(SUN_SHADOW_MAP_SIZE, SUN_SHADOW_MAP_SIZE);
|
|
107
|
+
light.shadow.map?.dispose();
|
|
108
|
+
light.shadow.map = null;
|
|
109
|
+
}
|
|
129
110
|
return light;
|
|
130
111
|
}
|
|
131
112
|
if (data.type === 'POINT') {
|
|
@@ -187,8 +168,8 @@ export function fitShadow(light: THREE.Light, centre: THREE.Vector3, radius: num
|
|
|
187
168
|
camera.updateProjectionMatrix();
|
|
188
169
|
}
|
|
189
170
|
|
|
190
|
-
/**
|
|
191
|
-
*
|
|
171
|
+
/** Aim directional and spot targets one unit down the authored -Z axis.
|
|
172
|
+
* Area lights emit along their own -Z without a target. */
|
|
192
173
|
export function aimLight(light: THREE.Light, node: THREE.Object3D): void {
|
|
193
174
|
// The parent owns the authored transform; directional/spot defaults are offset.
|
|
194
175
|
light.position.set(0, 0, 0);
|
|
@@ -251,54 +232,8 @@ export const SOLID_LIGHTS: readonly {
|
|
|
251
232
|
},
|
|
252
233
|
];
|
|
253
234
|
|
|
254
|
-
/**
|
|
255
|
-
|
|
256
|
-
* lights — their RELATIVE strengths, directions and wraps are Blender's own
|
|
257
|
-
* and nothing here tunes them apart.
|
|
258
|
-
*
|
|
259
|
-
* It absorbs two things this side cannot state exactly. First, the workbench's
|
|
260
|
-
* own normalization: Blender's shader is `Σ diffuseᵢ · wrap(N·Lᵢ)` times the
|
|
261
|
-
* viewport colour PLUS a specular term over `brdf_approx`, and the measured
|
|
262
|
-
* frame comes out at ~0.88 of the diffuse sum alone. Second, the stage's tone
|
|
263
|
-
* curve: the editor renders through ACES (`StageHost`), Blender's viewport
|
|
264
|
-
* through AgX, and near middle grey the two differ by a near-constant factor
|
|
265
|
-
* (checked both ways: with one gain ACES lands the three faces within 4 levels
|
|
266
|
-
* and AgX within 3, so the curve is NOT what row 1's 13-vs-30 spread was made
|
|
267
|
-
* of and the stage keeps its own operator).
|
|
268
|
-
*
|
|
269
|
-
* THE ROUNDS, on a `cyclotron create` scaffold driven through `cyclotron
|
|
270
|
-
* screenshot editor`, 30x30 means (std 0 — every patch inside one flat face)
|
|
271
|
-
* of the factory cube's three visible faces against Blender 5.2's own frame,
|
|
272
|
-
* (141,143,145) top / (129,131,131) left / (111,112,113) front:
|
|
273
|
-
*
|
|
274
|
-
* 0.74, ACES -> (148,150,151) (138,140,140) (108,110,110) worst 9
|
|
275
|
-
* 0.70, ACES -> (144,146,147) (134,136,136) (105,106,106) worst 7, spread 39
|
|
276
|
-
* 0.69, AgX -> (138,139,139) (131,132,133) (112,113,113) worst 6, spread 26
|
|
277
|
-
*
|
|
278
|
-
* Blender's own spread is 30. The ACES rounds are what says the tone curve
|
|
279
|
-
* was carrying the rest of row 1's defect: at ANY gain the operator holds the
|
|
280
|
-
* shading range 30% wider than Blender's, which is why the document names its
|
|
281
|
-
* own (`ToolViewportDressing.toneMapping`).
|
|
282
|
-
*
|
|
283
|
-
* WHAT IS LEFT, measured rather than guessed: the top face reads 3 to 6
|
|
284
|
-
* levels dark and slightly warm. Blender's workbench couples its specular to
|
|
285
|
-
* its diffuse — the same cube with `show_specular_highlight` off renders
|
|
286
|
-
* BRIGHTER, (146,148,149)/(136,138,138)/(114,115,116) against
|
|
287
|
-
* (142,144,145)/(130,132,132)/(112,113,113) with it on — and three's BRDF has
|
|
288
|
-
* no such coupling, nor a second colour per light to carry
|
|
289
|
-
* `specular_color`. Closing that is a shader of our own, not a light rig.
|
|
290
|
-
*/
|
|
291
|
-
/*
|
|
292
|
-
* SURFACES NO LONGER TAKE THIS RIG IN SOLID: they are drawn by Blender's own function
|
|
293
|
-
* (`blender-workbench-material.ts`), which reproduces Blender on the faces a single gain could
|
|
294
|
-
* not (a face square to the view: 158-160 against Blender's 161, where this rig gave 143). The rig
|
|
295
|
-
* still lights what that function does not draw (volume boxes). WHAT THE MEASUREMENT SHOWED about
|
|
296
|
-
* the rounds above: the three-face reference frames match Blender's function under the STANDARD
|
|
297
|
-
* view transform (function: 147/136/116 against 144/132/113; under AgX, 151/142/126), so they were
|
|
298
|
-
* made under Standard, and the gain was absorbing a view-transform difference, not a shading one.
|
|
299
|
-
* Blender's factory transform is AgX (`--factory-startup`: `view_transform` AgX), which the stage
|
|
300
|
-
* uses.
|
|
301
|
-
*/
|
|
235
|
+
/** One gain across the studio lights; relative strengths, directions and wraps remain Blender’s. */
|
|
236
|
+
/* Solid surfaces use blender-workbench-material.ts. This rig lights content outside that shader, including volume boxes, under the stage’s view transform. */
|
|
302
237
|
const SOLID_STUDIO_GAIN = 0.69;
|
|
303
238
|
|
|
304
239
|
/**
|
|
@@ -437,10 +372,10 @@ export type WorldExpression =
|
|
|
437
372
|
}
|
|
438
373
|
| {
|
|
439
374
|
kind: 'sky';
|
|
440
|
-
sky_model
|
|
441
|
-
sun_disc
|
|
442
|
-
sun_size
|
|
443
|
-
sun_intensity
|
|
375
|
+
sky_model: 'SINGLE_SCATTERING' | 'MULTIPLE_SCATTERING';
|
|
376
|
+
sun_disc: boolean;
|
|
377
|
+
sun_size: number;
|
|
378
|
+
sun_intensity: number;
|
|
444
379
|
sun_elevation: number;
|
|
445
380
|
sun_rotation: number;
|
|
446
381
|
altitude: number;
|
|
@@ -537,10 +472,10 @@ const worldExpression: z.ZodType<WorldExpression> = z.lazy(() =>
|
|
|
537
472
|
z
|
|
538
473
|
.object({
|
|
539
474
|
kind: z.literal('sky'),
|
|
540
|
-
sky_model: z.enum(['SINGLE_SCATTERING', 'MULTIPLE_SCATTERING'])
|
|
541
|
-
sun_disc: z.boolean()
|
|
542
|
-
sun_size: scalar
|
|
543
|
-
sun_intensity: scalar
|
|
475
|
+
sky_model: z.enum(['SINGLE_SCATTERING', 'MULTIPLE_SCATTERING']),
|
|
476
|
+
sun_disc: z.boolean(),
|
|
477
|
+
sun_size: scalar,
|
|
478
|
+
sun_intensity: scalar,
|
|
544
479
|
sun_elevation: scalar,
|
|
545
480
|
sun_rotation: scalar,
|
|
546
481
|
altitude: scalar,
|
|
@@ -574,36 +509,10 @@ export const worldSchema = worldRadiance
|
|
|
574
509
|
})
|
|
575
510
|
.strict();
|
|
576
511
|
|
|
577
|
-
/**
|
|
578
|
-
|
|
579
|
-
/** Sampled world radiance, by the description that produced it.
|
|
580
|
-
*
|
|
581
|
-
* Sampling is 32,768 texels and, for a SKY, each texel runs the
|
|
582
|
-
* multiple-scattering model -- synchronously, on the page's main thread.
|
|
583
|
-
* MEASURED: a capture of a scene with a sky world cost 15.4s against 0.03s for
|
|
584
|
-
* the same scene with a plain background, and three identical captures in a row
|
|
585
|
-
* each paid it in full. That is why the page froze solid during a replay and
|
|
586
|
-
* why the photograph was SLOWER than the Cycles path trace it is compared
|
|
587
|
-
* against (15.4s vs 7.6s) -- for a screenshot.
|
|
588
|
-
*
|
|
589
|
-
* The DATA is cached rather than the texture, because `clear()` disposes
|
|
590
|
-
* textures and a disposed texture must never be handed out again; a Uint16Array
|
|
591
|
-
* survives disposal and uploading it again is free. Keyed by the description
|
|
592
|
-
* itself, so any change to the sky (or to strength) rebuilds and an unchanged
|
|
593
|
-
* one never does. A handful of entries covers the background/lighting pair a
|
|
594
|
-
* split world needs without letting the map grow with the session. */
|
|
512
|
+
/** Cache sampled world-radiance arrays by description. Arrays survive texture disposal and can be uploaded into a new texture; bounded entries cover split background and lighting worlds. */
|
|
595
513
|
const WORLD_FIELD_CACHE_LIMIT = 4;
|
|
596
514
|
const worldFieldCache = new Map<string, Float32Array>();
|
|
597
515
|
|
|
598
|
-
/** Sky derivations still running. `worldReady()` is what a RENDER awaits, the
|
|
599
|
-
* same way it already awaits `lightingReady()` and `texturesReady()` -- a
|
|
600
|
-
* photograph gets one chance and must not catch a half-built sky. */
|
|
601
|
-
const pendingWorld = new Set<Promise<void>>();
|
|
602
|
-
|
|
603
|
-
export async function worldReady(): Promise<void> {
|
|
604
|
-
while (pendingWorld.size > 0) await Promise.all([...pendingWorld]);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
516
|
function worldTexture(
|
|
608
517
|
expression: WorldExpression,
|
|
609
518
|
strength: number,
|
|
@@ -611,11 +520,9 @@ function worldTexture(
|
|
|
611
520
|
): THREE.DataTexture {
|
|
612
521
|
const width = 256,
|
|
613
522
|
height = 128;
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
// sky, and made a world split by `Is Camera Ray` (one sky, two strengths)
|
|
618
|
-
// build that sky TWICE where once would do.
|
|
523
|
+
// Cache unscaled field samples by expression and any camera coordinates.
|
|
524
|
+
// Strength is applied per texture, so background and lighting branches at
|
|
525
|
+
// different strengths share the same sampled sky.
|
|
619
526
|
const windowCoordinates = usesWindowCoordinates(expression);
|
|
620
527
|
if (windowCoordinates && !camera)
|
|
621
528
|
throw new Error('World Window coordinates need a render camera');
|
|
@@ -626,19 +533,7 @@ function worldTexture(
|
|
|
626
533
|
]);
|
|
627
534
|
const cached = worldFieldCache.get(key);
|
|
628
535
|
if (cached !== undefined) return worldDataTexture(cached, strength, width, height);
|
|
629
|
-
//
|
|
630
|
-
// 512x256 multiple-scattering precompute -- has already been derived in the
|
|
631
|
-
// worker and primed into `blender-sky`'s cache by the time anything calls
|
|
632
|
-
// this; see `WorldBackground.apply`. What is left here is 32,768 bilinear
|
|
633
|
-
// lookups, which is milliseconds.
|
|
634
|
-
//
|
|
635
|
-
// It stays synchronous because the alternative is what shipped once and was
|
|
636
|
-
// taken back out (#6609): the texture handed out EMPTY and filled when the
|
|
637
|
-
// worker answered, with the capture racing the fill. Measured on pixels, a
|
|
638
|
-
// fresh sky rendered to 716 bytes and moving the sun produced the
|
|
639
|
-
// byte-identical empty image. A render gets one chance. Nothing here ever
|
|
640
|
-
// hands out a texture it has not already filled; the waiting happens one level
|
|
641
|
-
// up, where `pendingWorld` can hold the capture.
|
|
536
|
+
// Sky precomputation is primed by the worker before this synchronous sampling pass. Return only filled textures; WorldBackground.ready holds captures until the preparation finishes.
|
|
642
537
|
const windowPoint = new THREE.Vector3();
|
|
643
538
|
const projected = new THREE.Vector4();
|
|
644
539
|
const projection = camera
|
|
@@ -671,9 +566,6 @@ function worldTexture(
|
|
|
671
566
|
return worldDataTexture(samples, strength, width, height);
|
|
672
567
|
}
|
|
673
568
|
|
|
674
|
-
/** One sampled field, scaled, as a texture. The samples are UNSCALED and shared
|
|
675
|
-
* between textures; the multiply and the half-float conversion happen here, so
|
|
676
|
-
* two strengths over one sky cost one derivation and two cheap passes. */
|
|
677
569
|
/** Scale and convert, in one pass. The multiply happens BEFORE the half-float
|
|
678
570
|
* conversion, or a dim fill branch would quantise against a bright backdrop's
|
|
679
571
|
* range. */
|
|
@@ -692,6 +584,8 @@ function writeScaled(
|
|
|
692
584
|
}
|
|
693
585
|
}
|
|
694
586
|
|
|
587
|
+
/** Scale shared radiance samples into a texture, using float32 when the scaled
|
|
588
|
+
* field exceeds half-float range. */
|
|
695
589
|
function worldDataTexture(
|
|
696
590
|
samples: Float32Array,
|
|
697
591
|
strength: number,
|
|
@@ -724,7 +618,6 @@ export type WorldData = z.infer<typeof worldSchema>;
|
|
|
724
618
|
* Constants and linked colors use the same radiometric path. The environment
|
|
725
619
|
* samples the field at 256×128; features below a texel are filtered. */
|
|
726
620
|
export class WorldBackground {
|
|
727
|
-
lightingTexture():THREE.Texture|null {return this.environmentTexture??this.texture;}
|
|
728
621
|
readonly solarLights: THREE.DirectionalLight[] = [];
|
|
729
622
|
private readonly solarDirections = new Map<THREE.DirectionalLight, THREE.Vector3>();
|
|
730
623
|
|
|
@@ -763,23 +656,7 @@ export class WorldBackground {
|
|
|
763
656
|
* after the world moved on knows it is stale and composes nothing. */
|
|
764
657
|
private generation = 0;
|
|
765
658
|
|
|
766
|
-
/**
|
|
767
|
-
* THE ASYNC SEAM, and the only one.
|
|
768
|
-
*
|
|
769
|
-
* Every Sky Texture in this world is derived in a worker BEFORE anything is
|
|
770
|
-
* composed. When all of them are already in `blender-sky`'s cache the whole
|
|
771
|
-
* apply runs synchronously, exactly as it did inline. When any is missing,
|
|
772
|
-
* composition is DEFERRED behind the worker and the promise is registered in
|
|
773
|
-
* `pendingWorld` SYNCHRONOUSLY, before this method returns -- which is what
|
|
774
|
-
* makes it impossible for a caller to reach `worldReady()` in between.
|
|
775
|
-
* `BlenderRuntimeView.setRendered` awaits `worldReady()` AFTER the applies for
|
|
776
|
-
* this reason, and that ordering must not move.
|
|
777
|
-
*
|
|
778
|
-
* The failure this is shaped against (#6609) was a texture handed out empty
|
|
779
|
-
* and filled later. Here nothing is handed out early at all: until the sky
|
|
780
|
-
* exists, `scene.background` is simply not assigned, and the capture that
|
|
781
|
-
* awaits `worldReady()` cannot run until it is.
|
|
782
|
-
*/
|
|
659
|
+
/** Prepare missing sky textures in the worker and publish pending synchronously before returning. Assign the composed background only after preparation; captures await ready after apply. */
|
|
783
660
|
apply(root: THREE.Object3D, world: WorldData | null, camera?: THREE.Camera): void {
|
|
784
661
|
this.clear();
|
|
785
662
|
this.pending = null;
|
|
@@ -812,22 +689,18 @@ export class WorldBackground {
|
|
|
812
689
|
presenterChanged();
|
|
813
690
|
})();
|
|
814
691
|
this.pending = pending;
|
|
815
|
-
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
void pending.then(
|
|
820
|
-
() => pendingWorld.delete(pending),
|
|
821
|
-
() => pendingWorld.delete(pending),
|
|
822
|
-
);
|
|
692
|
+
// Handled here, or a rejection nobody has awaited yet is an unhandled one;
|
|
693
|
+
// `ready()` still awaits `pending` itself, so the failure reaches the
|
|
694
|
+
// capture LOUDLY rather than leaving it to photograph a scene with no sky.
|
|
695
|
+
pending.catch(() => {});
|
|
823
696
|
}
|
|
824
697
|
|
|
825
698
|
private compose(
|
|
826
699
|
scene: THREE.Scene,
|
|
827
700
|
world: WorldData,
|
|
828
701
|
expression: WorldExpression,
|
|
829
|
-
camera
|
|
830
|
-
root
|
|
702
|
+
camera: THREE.Camera | undefined,
|
|
703
|
+
root: THREE.Object3D,
|
|
831
704
|
): void {
|
|
832
705
|
this.texture = worldTexture(expression, world.strength, camera);
|
|
833
706
|
if (usesWindowCoordinates(expression)) {
|
|
@@ -853,11 +726,8 @@ export class WorldBackground {
|
|
|
853
726
|
} else {
|
|
854
727
|
scene.background = this.texture;
|
|
855
728
|
}
|
|
856
|
-
// Replace the document environment
|
|
857
|
-
//
|
|
858
|
-
// radiance from what the camera photographs -- `19-tram-stop` lights at
|
|
859
|
-
// 0.30 behind a 0.95 backdrop -- and three keeps the two apart, so using
|
|
860
|
-
// the backdrop here would light that scene three times too brightly.
|
|
729
|
+
// Replace the document environment. A world split by `Is Camera Ray`
|
|
730
|
+
// supplies separate lighting and backdrop radiance.
|
|
861
731
|
const { lighting } = world;
|
|
862
732
|
const lightingExpression = lighting?.shader ?? lighting?.color ?? expression;
|
|
863
733
|
const solar = worldSolarIrradiance(lightingExpression);
|
|
@@ -866,15 +736,15 @@ export class WorldBackground {
|
|
|
866
736
|
lighting?.strength ?? world.strength, camera)
|
|
867
737
|
: null;
|
|
868
738
|
scene.environment = this.environmentTexture ?? this.texture;
|
|
869
|
-
root
|
|
739
|
+
root.updateMatrixWorld(true);
|
|
870
740
|
for (const {direction, irradiance} of solar) {
|
|
871
741
|
const light = new THREE.DirectionalLight(0xffffff, lighting?.strength ?? world.strength);
|
|
872
742
|
light.name = 'BlenderWorldSun';
|
|
873
743
|
light.color.setRGB(irradiance.x, irradiance.y, irradiance.z, THREE.LinearSRGBColorSpace);
|
|
874
744
|
light.position.copy(direction);
|
|
875
|
-
|
|
745
|
+
light.position.transformDirection(root.matrixWorld);
|
|
876
746
|
light.castShadow = true;
|
|
877
|
-
light.shadow.mapSize.set(
|
|
747
|
+
light.shadow.mapSize.set(SUN_SHADOW_MAP_SIZE, SUN_SHADOW_MAP_SIZE);
|
|
878
748
|
scene.add(light, light.target);
|
|
879
749
|
this.solarLights.push(light);
|
|
880
750
|
this.solarDirections.set(light, direction.clone());
|
|
@@ -71,7 +71,8 @@ export function fitModelDirectionalShadow(
|
|
|
71
71
|
);
|
|
72
72
|
const size = bounds.getSize(new THREE.Vector3());
|
|
73
73
|
const epsilon = Math.max(size.length() * 1e-6, Number.EPSILON);
|
|
74
|
-
//
|
|
74
|
+
// PCF (r186: five taps on a disk of `shadow.radius`, 1 texel, each a
|
|
75
|
+
// hardware 2x2 compare) reaches two texels per axis; the normal offset can move the
|
|
75
76
|
// lookup another 2*sqrt(2) texels. Reserve that same footprint for both
|
|
76
77
|
// the map and the casters that can contribute to its edge samples.
|
|
77
78
|
const support = 2 + 2 * Math.SQRT2;
|
|
@@ -110,3 +111,188 @@ export function fitModelDirectionalShadow(
|
|
|
110
111
|
camera.updateProjectionMatrix();
|
|
111
112
|
light.shadow.normalBias = normalBias;
|
|
112
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* HOW FAR A POINT OR SPOT LIGHT REACHES, as EEVEE cuts it off (`Light::attenuation_radius_get`): where its power,
|
|
117
|
+
* falling with the square of the distance, drops under the scene's light threshold, sqrt(max colour · watts / 100 /
|
|
118
|
+
* threshold) metres, at EEVEE's default threshold (0.01, which the frame does not carry), so sqrt(max colour · watts).
|
|
119
|
+
* In three.js's terms the watts are 4π times the intensity (`blender-runtime-lighting.ts`). A three.js cut-off
|
|
120
|
+
* (`distance`), when one is set, is a reach too.
|
|
121
|
+
*/
|
|
122
|
+
export function perspectiveLightReach(light: THREE.PointLight | THREE.SpotLight): number {
|
|
123
|
+
const watts = 4 * Math.PI * Math.max(0, light.intensity);
|
|
124
|
+
const reach = Math.sqrt(Math.max(light.color.r, light.color.g, light.color.b, 0) * watts / 100 / EEVEE_LIGHT_THRESHOLD);
|
|
125
|
+
return light.distance > 0 ? Math.min(light.distance, reach) : reach;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** EEVEE's default light threshold (`scene.eevee.light_threshold`): below it a light lights nothing. */
|
|
129
|
+
const EEVEE_LIGHT_THRESHOLD = 0.01;
|
|
130
|
+
|
|
131
|
+
/** Find the farthest visible shadow receiver, skipping boxes whose farthest corner cannot improve the current result. Return a one-point array, or an empty array when no receiver is visible. */
|
|
132
|
+
export function farthestVisibleReceiver(boxes: readonly THREE.Box3[], frustum: THREE.Frustum,
|
|
133
|
+
from: THREE.Vector3): THREE.Vector3[] {
|
|
134
|
+
// A box's farthest corner from `from`: on each axis, the face farther from it.
|
|
135
|
+
const farthest = (box: THREE.Box3): number => box.isEmpty() ? -1 : Math.hypot(
|
|
136
|
+
Math.max(Math.abs(box.min.x - from.x), Math.abs(box.max.x - from.x)),
|
|
137
|
+
Math.max(Math.abs(box.min.y - from.y), Math.abs(box.max.y - from.y)),
|
|
138
|
+
Math.max(Math.abs(box.min.z - from.z), Math.abs(box.max.z - from.z)));
|
|
139
|
+
const reach = boxes.map((box) => ({ box, far: farthest(box) })).sort((a, b) => b.far - a.far);
|
|
140
|
+
let best = -1;
|
|
141
|
+
let point: THREE.Vector3 | null = null;
|
|
142
|
+
for (const { box, far } of reach) {
|
|
143
|
+
if (far <= best) break;
|
|
144
|
+
for (const candidate of visibleShadowReceivers(box, frustum)) {
|
|
145
|
+
const distance = candidate.distanceTo(from);
|
|
146
|
+
if (distance > best) { best = distance; point = candidate; }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return point ? [point] : [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Size a point or spot light’s normal offset from a shadow-map texel at its farthest visible lit receiver. With no receiver, use the nearer of the light’s reach and far plane. Point maps have 90-degree faces; spot maps span 2 × angle × focus. */
|
|
153
|
+
export function fitPerspectiveShadowBias(
|
|
154
|
+
light: THREE.PointLight | THREE.SpotLight,
|
|
155
|
+
receivers: readonly THREE.Vector3[],
|
|
156
|
+
): void {
|
|
157
|
+
if (!light.castShadow) return;
|
|
158
|
+
const position = light.getWorldPosition(new THREE.Vector3());
|
|
159
|
+
const far = (light.shadow.camera as THREE.PerspectiveCamera).far;
|
|
160
|
+
const reach = Math.min(perspectiveLightReach(light), far);
|
|
161
|
+
let distance = 0;
|
|
162
|
+
for (const point of receivers) distance = Math.max(distance, point.distanceTo(position));
|
|
163
|
+
distance = distance > 0 ? Math.min(distance, reach) : reach;
|
|
164
|
+
const half = (light as THREE.SpotLight).isSpotLight
|
|
165
|
+
? Math.min((light as THREE.SpotLight).angle * (light.shadow as THREE.SpotLightShadow).focus, Math.PI / 2 - 1e-3)
|
|
166
|
+
: Math.PI / 4;
|
|
167
|
+
const size = Math.max(1, Math.min(light.shadow.mapSize.x, light.shadow.mapSize.y));
|
|
168
|
+
const texel = (2 * distance * Math.tan(half)) / size;
|
|
169
|
+
light.shadow.normalBias = 2 * Math.SQRT2 * texel;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const RECEIVER_VERTEX = /* glsl */ `
|
|
173
|
+
#include <common>
|
|
174
|
+
#include <batching_pars_vertex>
|
|
175
|
+
#include <morphtarget_pars_vertex>
|
|
176
|
+
#include <skinning_pars_vertex>
|
|
177
|
+
#include <logdepthbuf_pars_vertex>
|
|
178
|
+
#include <clipping_planes_pars_vertex>
|
|
179
|
+
varying vec3 vReceiver;
|
|
180
|
+
void main() {
|
|
181
|
+
#include <batching_vertex>
|
|
182
|
+
#include <skinbase_vertex>
|
|
183
|
+
#include <morphinstance_vertex>
|
|
184
|
+
#include <begin_vertex>
|
|
185
|
+
#include <morphtarget_vertex>
|
|
186
|
+
#include <skinning_vertex>
|
|
187
|
+
#include <project_vertex>
|
|
188
|
+
#include <logdepthbuf_vertex>
|
|
189
|
+
#include <clipping_planes_vertex>
|
|
190
|
+
vReceiver = mvPosition.xyz;
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
const RECEIVER_FRAGMENT = /* glsl */ `
|
|
195
|
+
varying vec3 vReceiver;
|
|
196
|
+
#include <logdepthbuf_pars_fragment>
|
|
197
|
+
#include <clipping_planes_pars_fragment>
|
|
198
|
+
void main() {
|
|
199
|
+
#include <clipping_planes_fragment>
|
|
200
|
+
#include <logdepthbuf_fragment>
|
|
201
|
+
gl_FragColor = vec4(vReceiver, 1.0);
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
|
|
205
|
+
/** Samples across the drawing's width; the height keeps the drawing's aspect. */
|
|
206
|
+
const RECEIVER_SAMPLES = 256;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Fit sun shadows to the surfaces visible through the drawing camera, following EEVEE’s
|
|
210
|
+
* receiver-depth approach. Render view-space positions at a small sample grid spanning the
|
|
211
|
+
* drawing’s edges, then return those samples in world space. Hidden surfaces are excluded so the
|
|
212
|
+
* geometry behind them can contribute.
|
|
213
|
+
*/
|
|
214
|
+
export class VisibleReceivers {
|
|
215
|
+
private target: THREE.WebGLRenderTarget | null = null;
|
|
216
|
+
private pixels = new Float32Array(0);
|
|
217
|
+
private readonly camera = new THREE.Camera();
|
|
218
|
+
private readonly material = new THREE.ShaderMaterial({
|
|
219
|
+
name: 'Blender visible receivers',
|
|
220
|
+
vertexShader: RECEIVER_VERTEX,
|
|
221
|
+
fragmentShader: RECEIVER_FRAGMENT,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
/** World-space points of what `camera` sees of `scene`; empty when nothing (or no readable
|
|
225
|
+
* float target) answered, which a caller treats as "no samples". */
|
|
226
|
+
sample(
|
|
227
|
+
renderer: THREE.WebGLRenderer,
|
|
228
|
+
scene: THREE.Scene,
|
|
229
|
+
camera: THREE.Camera,
|
|
230
|
+
hidden: readonly THREE.Object3D[],
|
|
231
|
+
): THREE.Vector3[] {
|
|
232
|
+
const output = renderer.getRenderTarget();
|
|
233
|
+
const size = output ? new THREE.Vector2(output.width, output.height) : renderer.getDrawingBufferSize(new THREE.Vector2());
|
|
234
|
+
const width = RECEIVER_SAMPLES;
|
|
235
|
+
const height = Math.max(2, Math.round((RECEIVER_SAMPLES * size.y) / Math.max(1, size.x)));
|
|
236
|
+
this.target ??= new THREE.WebGLRenderTarget(width, height, {
|
|
237
|
+
type: THREE.FloatType, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter,
|
|
238
|
+
generateMipmaps: false, depthBuffer: true,
|
|
239
|
+
});
|
|
240
|
+
if (this.target.width !== width || this.target.height !== height) this.target.setSize(width, height);
|
|
241
|
+
// The outermost sample centres on the drawing's own edges: scaling the projection's x and y
|
|
242
|
+
// by (n - 1) / n puts sample i of n at (2i + 1 - n) / (n - 1) in the drawing's NDC.
|
|
243
|
+
camera.updateMatrixWorld();
|
|
244
|
+
const probe = this.camera;
|
|
245
|
+
probe.matrixAutoUpdate = false;
|
|
246
|
+
probe.matrix.copy(camera.matrixWorld);
|
|
247
|
+
probe.matrixWorld.copy(camera.matrixWorld);
|
|
248
|
+
probe.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
249
|
+
probe.projectionMatrix.copy(camera.projectionMatrix)
|
|
250
|
+
.premultiply(new THREE.Matrix4().makeScale((width - 1) / width, (height - 1) / height, 1));
|
|
251
|
+
probe.projectionMatrixInverse.copy(probe.projectionMatrix).invert();
|
|
252
|
+
probe.layers.mask = camera.layers.mask;
|
|
253
|
+
const count = width * height * 4;
|
|
254
|
+
if (this.pixels.length !== count) this.pixels = new Float32Array(count);
|
|
255
|
+
this.pixels.fill(0);
|
|
256
|
+
const visible = hidden.map((object) => object.visible);
|
|
257
|
+
const background = scene.background;
|
|
258
|
+
const override = scene.overrideMaterial;
|
|
259
|
+
const autoClear = renderer.autoClear;
|
|
260
|
+
const shadows = renderer.shadowMap.enabled;
|
|
261
|
+
const clearColor = renderer.getClearColor(new THREE.Color());
|
|
262
|
+
const clearAlpha = renderer.getClearAlpha();
|
|
263
|
+
try {
|
|
264
|
+
for (const object of hidden) object.visible = false;
|
|
265
|
+
scene.background = null;
|
|
266
|
+
scene.overrideMaterial = this.material;
|
|
267
|
+
renderer.autoClear = false;
|
|
268
|
+
renderer.shadowMap.enabled = false;
|
|
269
|
+
renderer.setRenderTarget(this.target);
|
|
270
|
+
renderer.setClearColor(0x000000, 0);
|
|
271
|
+
renderer.clear(true, true, false);
|
|
272
|
+
renderer.render(scene, probe);
|
|
273
|
+
renderer.readRenderTargetPixels(this.target, 0, 0, width, height, this.pixels);
|
|
274
|
+
} finally {
|
|
275
|
+
hidden.forEach((object, index) => { object.visible = visible[index]!; });
|
|
276
|
+
scene.background = background;
|
|
277
|
+
scene.overrideMaterial = override;
|
|
278
|
+
renderer.autoClear = autoClear;
|
|
279
|
+
renderer.shadowMap.enabled = shadows;
|
|
280
|
+
renderer.setClearColor(clearColor, clearAlpha);
|
|
281
|
+
renderer.setRenderTarget(output);
|
|
282
|
+
}
|
|
283
|
+
const points: THREE.Vector3[] = [];
|
|
284
|
+
for (let i = 0; i < width * height; i++) {
|
|
285
|
+
if (this.pixels[i * 4 + 3]! === 0) continue;
|
|
286
|
+
points.push(new THREE.Vector3(this.pixels[i * 4]!, this.pixels[i * 4 + 1]!, this.pixels[i * 4 + 2]!)
|
|
287
|
+
.applyMatrix4(camera.matrixWorld));
|
|
288
|
+
}
|
|
289
|
+
return points;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
dispose(): void {
|
|
293
|
+
this.target?.dispose();
|
|
294
|
+
this.target = null;
|
|
295
|
+
this.material.dispose();
|
|
296
|
+
this.pixels = new Float32Array(0);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -74,8 +74,57 @@ export function skinGeometry(geometry: THREE.BufferGeometry, skin: FrameSkin): v
|
|
|
74
74
|
geometry.userData['blenderSkin'] = { armature: skin.armature, groups: [...skin.groups] };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* A SKINNED MESH'S REACH: how far any of its vertices lies from a bone that moves it, at the bind.
|
|
79
|
+
* Skinning moves a vertex rigidly with each bone and blends the results, so wherever the bones go,
|
|
80
|
+
* every vertex stays within this distance of the space the bones span. `fitBounds` makes the
|
|
81
|
+
* frame's bounding sphere from it.
|
|
82
|
+
*/
|
|
83
|
+
function skinReach(skinned: THREE.SkinnedMesh): number {
|
|
84
|
+
const position = skinned.geometry.getAttribute('position');
|
|
85
|
+
const index = skinned.geometry.getAttribute('skinIndex');
|
|
86
|
+
const weight = skinned.geometry.getAttribute('skinWeight');
|
|
87
|
+
if (!position || !index || !weight) return Infinity;
|
|
88
|
+
const joints = skinned.skeleton.boneInverses.map((inverse) =>
|
|
89
|
+
new THREE.Vector3().setFromMatrixPosition(inverse.clone().invert()));
|
|
90
|
+
const vertex = new THREE.Vector3();
|
|
91
|
+
let reach = 0;
|
|
92
|
+
for (let i = 0; i < position.count; i++) {
|
|
93
|
+
vertex.fromBufferAttribute(position, i).applyMatrix4(skinned.bindMatrix);
|
|
94
|
+
for (let k = 0; k < 4; k++) {
|
|
95
|
+
if (weight.getComponent(i, k) <= 0) continue;
|
|
96
|
+
const joint = joints[index.getComponent(i, k)];
|
|
97
|
+
if (joint) reach = Math.max(reach, vertex.distanceTo(joint));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return reach;
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
export class RuntimeSkeletons {
|
|
78
104
|
#rigs = new Map<string, Rig>();
|
|
105
|
+
/** Every mesh `bind` skinned, with its reach (`skinReach`). */
|
|
106
|
+
#bound = new Map<THREE.SkinnedMesh, number>();
|
|
107
|
+
readonly #box = new THREE.Box3();
|
|
108
|
+
readonly #point = new THREE.Vector3();
|
|
109
|
+
readonly #inverse = new THREE.Matrix4();
|
|
110
|
+
|
|
111
|
+
/** Refresh skinned-mesh culling spheres from current world-space bone positions plus each mesh’s reach. prepareDraw calls this after updateMatrixWorld. */
|
|
112
|
+
fitBounds(): void {
|
|
113
|
+
for (const [skinned, reach] of this.#bound) {
|
|
114
|
+
// Disable culling for detached meshes and skins whose reach cannot be read.
|
|
115
|
+
if (!skinned.parent) { skinned.frustumCulled = false; this.#bound.delete(skinned); continue; }
|
|
116
|
+
if (!Number.isFinite(reach)) { skinned.frustumCulled = false; continue; }
|
|
117
|
+
const box = this.#box.makeEmpty();
|
|
118
|
+
for (const bone of skinned.skeleton.bones) box.expandByPoint(this.#point.setFromMatrixPosition(bone.matrixWorld));
|
|
119
|
+
const sphere = skinned.boundingSphere ?? (skinned.boundingSphere = new THREE.Sphere());
|
|
120
|
+
box.getBoundingSphere(sphere);
|
|
121
|
+
sphere.radius += reach;
|
|
122
|
+
// Into the mesh's own space, where three reads the sphere (`Frustum.intersectsObject`).
|
|
123
|
+
this.#inverse.copy(skinned.matrixWorld).invert();
|
|
124
|
+
sphere.applyMatrix4(this.#inverse);
|
|
125
|
+
skinned.frustumCulled = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
79
128
|
|
|
80
129
|
/** The rig of one armature, or null when the frame names none by that name. */
|
|
81
130
|
rig(armature: string): ArmatureRig | null {
|
|
@@ -130,7 +179,7 @@ export class RuntimeSkeletons {
|
|
|
130
179
|
rig = { armature: name, object, root, still, bones, structure, pose: new Map(), poseKey: '', action: null };
|
|
131
180
|
this.#rigs.set(name, rig);
|
|
132
181
|
}
|
|
133
|
-
rig.action = armature.action
|
|
182
|
+
rig.action = armature.action;
|
|
134
183
|
const poseKey = armature.bones.map((bone) => bone.matrix.flat().join(',')).join(';');
|
|
135
184
|
if (poseKey !== rig.poseKey) {
|
|
136
185
|
rig.poseKey = poseKey;
|
|
@@ -196,11 +245,14 @@ export class RuntimeSkeletons {
|
|
|
196
245
|
// A re-export rebinds the same mesh: its old skeleton's bone texture goes with it.
|
|
197
246
|
(skinned.skeleton as THREE.Skeleton | undefined)?.dispose();
|
|
198
247
|
skinned.bind(new THREE.Skeleton(bones, inverses), mesh.matrixWorld.clone());
|
|
248
|
+
if (skinned !== mesh) this.#bound.delete(mesh as THREE.SkinnedMesh);
|
|
249
|
+
this.#bound.set(skinned, skinReach(skinned));
|
|
199
250
|
return skinned;
|
|
200
251
|
}
|
|
201
252
|
|
|
202
253
|
dispose(): void {
|
|
203
254
|
for (const rig of this.#rigs.values()) rig.root.removeFromParent();
|
|
204
255
|
this.#rigs.clear();
|
|
256
|
+
this.#bound.clear();
|
|
205
257
|
}
|
|
206
258
|
}
|