@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.
Files changed (245) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +4 -11
  3. package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
  4. package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
  5. package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
  6. package/browser/addons/vrm/README.txt +3 -0
  7. package/browser/addons/vrm/__init__.py +41 -0
  8. package/browser/addons/vrm/blender_manifest.toml +16 -0
  9. package/browser/addons/vrm/common/__init__.py +1 -0
  10. package/browser/addons/vrm/common/animation.py +58 -0
  11. package/browser/addons/vrm/common/blender_manifest.py +82 -0
  12. package/browser/addons/vrm/common/char.py +16 -0
  13. package/browser/addons/vrm/common/convert.py +287 -0
  14. package/browser/addons/vrm/common/convert_any.py +48 -0
  15. package/browser/addons/vrm/common/debug.py +40 -0
  16. package/browser/addons/vrm/common/deep.py +162 -0
  17. package/browser/addons/vrm/common/error_dialog.py +274 -0
  18. package/browser/addons/vrm/common/fs.py +71 -0
  19. package/browser/addons/vrm/common/gl.py +20 -0
  20. package/browser/addons/vrm/common/gltf.py +1149 -0
  21. package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
  22. package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
  23. package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
  24. package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
  25. package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
  26. package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
  27. package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
  28. package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
  29. package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
  30. package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
  31. package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
  32. package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
  33. package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
  34. package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
  35. package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
  36. package/browser/addons/vrm/common/legacy_gltf.py +16 -0
  37. package/browser/addons/vrm/common/logger.py +84 -0
  38. package/browser/addons/vrm/common/mtoon0.blend +0 -0
  39. package/browser/addons/vrm/common/mtoon1.blend +0 -0
  40. package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
  41. package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
  42. package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
  43. package/browser/addons/vrm/common/ops/__init__.py +10 -0
  44. package/browser/addons/vrm/common/ops/export_scene.py +67 -0
  45. package/browser/addons/vrm/common/ops/icyp.py +53 -0
  46. package/browser/addons/vrm/common/ops/import_scene.py +57 -0
  47. package/browser/addons/vrm/common/ops/vrm.py +2380 -0
  48. package/browser/addons/vrm/common/ops/wm.py +167 -0
  49. package/browser/addons/vrm/common/preferences.py +362 -0
  50. package/browser/addons/vrm/common/progress.py +87 -0
  51. package/browser/addons/vrm/common/rotation.py +89 -0
  52. package/browser/addons/vrm/common/safe_removal.py +46 -0
  53. package/browser/addons/vrm/common/scene_watcher.py +230 -0
  54. package/browser/addons/vrm/common/shader.py +2243 -0
  55. package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
  56. package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
  57. package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
  58. package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
  59. package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
  60. package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
  61. package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
  62. package/browser/addons/vrm/common/version.py +161 -0
  63. package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
  64. package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
  65. package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
  66. package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
  67. package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
  68. package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
  69. package/browser/addons/vrm/common/workspace.py +227 -0
  70. package/browser/addons/vrm/common/writable_context.py +77 -0
  71. package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
  72. package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
  73. package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
  74. package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
  75. package/browser/addons/vrm/documentation/en-us/index.md +41 -0
  76. package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
  77. package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
  78. package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
  79. package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
  80. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
  81. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
  82. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
  83. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
  84. package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
  85. package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
  86. package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
  87. package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
  88. package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
  89. package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
  90. package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
  91. package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
  92. package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
  93. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
  94. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
  95. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
  96. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
  97. package/browser/addons/vrm/editor/__init__.py +1 -0
  98. package/browser/addons/vrm/editor/extension.py +425 -0
  99. package/browser/addons/vrm/editor/extension_accessor.py +75 -0
  100. package/browser/addons/vrm/editor/handler.py +84 -0
  101. package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
  102. package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
  103. package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
  104. package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
  105. package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
  106. package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
  107. package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
  108. package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
  109. package/browser/addons/vrm/editor/make_armature.py +709 -0
  110. package/browser/addons/vrm/editor/menu.py +147 -0
  111. package/browser/addons/vrm/editor/migration.py +297 -0
  112. package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
  113. package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
  114. package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
  115. package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
  116. package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
  117. package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
  118. package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
  119. package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
  120. package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
  121. package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
  122. package/browser/addons/vrm/editor/ops.py +471 -0
  123. package/browser/addons/vrm/editor/panel.py +315 -0
  124. package/browser/addons/vrm/editor/property_group.py +888 -0
  125. package/browser/addons/vrm/editor/search.py +682 -0
  126. package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
  127. package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
  128. package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
  129. package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
  130. package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
  131. package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
  132. package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
  133. package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
  134. package/browser/addons/vrm/editor/subscription.py +123 -0
  135. package/browser/addons/vrm/editor/t_pose.py +713 -0
  136. package/browser/addons/vrm/editor/validation.py +1271 -0
  137. package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
  138. package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
  139. package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
  140. package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
  141. package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
  142. package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
  143. package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
  144. package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
  145. package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
  146. package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
  147. package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
  148. package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
  149. package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
  150. package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
  151. package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
  152. package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
  153. package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
  154. package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
  155. package/browser/addons/vrm/exporter/__init__.py +1 -0
  156. package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
  157. package/browser/addons/vrm/exporter/export_scene.py +930 -0
  158. package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
  159. package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
  160. package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
  161. package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
  162. package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
  163. package/browser/addons/vrm/external/__init__.py +1 -0
  164. package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
  165. package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
  166. package/browser/addons/vrm/external/rigify_support.py +1 -0
  167. package/browser/addons/vrm/importer/__init__.py +1 -0
  168. package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
  169. package/browser/addons/vrm/importer/file_handler.py +34 -0
  170. package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
  171. package/browser/addons/vrm/importer/import_scene.py +646 -0
  172. package/browser/addons/vrm/importer/license_validation.py +198 -0
  173. package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
  174. package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
  175. package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
  176. package/browser/addons/vrm/importer/vrm_diff.py +316 -0
  177. package/browser/addons/vrm/locale/__init__.py +1 -0
  178. package/browser/addons/vrm/locale/ja_jp.py +826 -0
  179. package/browser/addons/vrm/locale/ruff.toml +8 -0
  180. package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
  181. package/browser/addons/vrm/locale/zh_hans.py +928 -0
  182. package/browser/addons/vrm/registration.py +670 -0
  183. package/browser/addons/vrm.json +18 -0
  184. package/browser/blender-emscripten-engine.mts +60 -128
  185. package/browser/blender-engine.mts +20 -186
  186. package/browser/blender-wali-engine.mts +35 -81
  187. package/browser/describe-thrown.mts +3 -6
  188. package/browser/document-chunks.mts +4 -16
  189. package/browser/gltf_export.py +3161 -0
  190. package/browser/heap-growth.mts +23 -0
  191. package/browser/index.ts +2 -0
  192. package/browser/protocol.ts +119 -35
  193. package/browser/pull-job.mts +2 -10
  194. package/browser/rna.ts +190 -73
  195. package/browser/runtime.ts +197 -82
  196. package/browser/session-addons.mts +33 -0
  197. package/browser/session-frame.mts +5 -24
  198. package/browser/session.py +1338 -1427
  199. package/browser/three/attach-presenter.ts +11 -39
  200. package/browser/three/blender-base64.ts +1 -36
  201. package/browser/three/blender-corner-normals.ts +1 -23
  202. package/browser/three/blender-display-shaders.json +1 -1
  203. package/browser/three/blender-display-shaders.lut +0 -0
  204. package/browser/three/blender-display-transform.ts +2 -1
  205. package/browser/three/blender-frame-columns.ts +5 -13
  206. package/browser/three/blender-graph-material.ts +18 -3
  207. package/browser/three/blender-node-graph.ts +10 -34
  208. package/browser/three/blender-physical-material.ts +2 -31
  209. package/browser/three/blender-runtime-armature.ts +7 -60
  210. package/browser/three/blender-runtime-frame.ts +9 -23
  211. package/browser/three/blender-runtime-geometry.ts +22 -26
  212. package/browser/three/blender-runtime-lighting.ts +48 -178
  213. package/browser/three/blender-runtime-shadows.ts +187 -1
  214. package/browser/three/blender-runtime-skeleton.ts +53 -1
  215. package/browser/three/blender-runtime-view.ts +505 -283
  216. package/browser/three/blender-runtime-weights.ts +9 -72
  217. package/browser/three/blender-sky.ts +13 -33
  218. package/browser/three/blender-triangulate.ts +7 -35
  219. package/browser/three/blender-viewport-sampling.ts +235 -0
  220. package/browser/three/blender-workbench-material.ts +25 -29
  221. package/browser/three/blender-workbench-outline.ts +199 -0
  222. package/browser/three/blender-world-volume.ts +62 -10
  223. package/browser/three/presenter.ts +10 -26
  224. package/browser/three/release.ts +5 -21
  225. package/browser/three/sky-precompute-worker.ts +1 -18
  226. package/browser/three/sky-worker.ts +5 -23
  227. package/browser/three/world-field-sampler.ts +12 -26
  228. package/browser/volter_three.py +9 -105
  229. package/browser/worker.ts +156 -151
  230. package/package.json +5 -2
  231. package/wasm/BUNDLE.json +17 -17
  232. package/wasm/DEPENDENCY-LICENSES.txt +14 -0
  233. package/wasm/blender_browser.data.br +0 -0
  234. package/wasm/blender_browser.js +1 -1
  235. package/wasm/blender_browser.wasm.br +0 -0
  236. package/browser/frame-transfer.mts +0 -19
  237. package/browser/three/agx-base-srgb.lut +0 -0
  238. package/browser/three/agx-look-medium-high-contrast.lut +0 -0
  239. package/browser/three/agx-look-punchy.lut +0 -0
  240. package/browser/three/blender-agx.ts +0 -235
  241. package/browser/three/blender-display-lut.ts +0 -145
  242. package/browser/three/blender-filmic.ts +0 -49
  243. package/browser/three/blender-standard.ts +0 -68
  244. package/browser/three/blender-world-visibility.ts +0 -278
  245. package/browser/three/filmic-srgb.lut +0 -0
@@ -1,30 +1,7 @@
1
- /**
2
- * THE WEIGHT OVERLAY — the active vertex group's weights, coloured with
3
- * Blender's own ramp (ARCHITECTURE-CORE §Blender north star, "Inspection
4
- * parity, not editing parity" and "The reference is Blender's SOURCE as well
5
- * as its frames"; WORK.md §Blender in the tab is Blender, "Inspection parity",
6
- * I4).
7
- *
8
- * IT IS INSPECTION, NOT PAINT MODE. Blender shows these colours by entering
9
- * Weight Paint; here it is a viewport overlay toggle beside Bones, because the
10
- * Model document has no brushes to enter a paint mode for and looking at the
11
- * weights is the whole point.
12
- *
13
- * WHICH RAMP, and the correction it cost. The brief for this unit named
14
- * `BKE_defvert_weight_to_rgb` / `weight_to_rgb`
15
- * (`blenkernel/intern/deform.cc:1559-1590`) — the blue → cyan → green →
16
- * yellow → red piecewise ramp. MEASURED at the engine's pin, that function is
17
- * NOT what paints a vertex group: its only callers in the whole tree are
18
- * `blenkernel/intern/particle.cc:3602,3624-3625`, the HAIR KEY weight colour.
19
- * The weight-paint viewport samples a 256-texel 1D table built in
20
- * `draw/engines/overlay/overlay_instance.cc:186-221`, and that table is an HSV
21
- * sweep with a gamma correction — a different curve with the same endpoints,
22
- * which is why the two look alike until you compare mid-tones. This file is
23
- * the table Blender actually samples; the piecewise ramp is named here so the
24
- * next reader does not "fix" it back.
25
- */
1
+
26
2
  import * as THREE from 'three';
27
3
  import { z } from 'zod';
4
+ import { bytesFromBase64 } from './blender-base64';
28
5
 
29
6
  /** What `session.py`'s `_weights` answers. `weightsBase64` / `alertBase64` are
30
7
  * absent on a REFERENCE (`unchanged`), which is the same contract the meshes
@@ -36,10 +13,8 @@ export const weightsSchema = z
36
13
  group: z.string(),
37
14
  groupIndex: z.number().int().nonnegative(),
38
15
  count: z.number().int().nonnegative(),
39
- /** sha1 of the packed weight + alert bytes. The reference key, and the
40
- * presenter's staleness signal — never the mesh's revision, which does
41
- * NOT move when only deform weights change (`session.py::_weights` states
42
- * the measurement). */
16
+ /** sha1 of the packed weight and alert bytes: the reference key and
17
+ * staleness signal, independent of mesh geometry revisions. */
43
18
  digest: z.string(),
44
19
  /** `scene.tool_settings.vertex_group_user` (`rna_scene.cc:3428-3434`). */
45
20
  alertMode: z.enum(['NONE', 'ACTIVE', 'ALL']),
@@ -126,27 +101,9 @@ function hueToRgb(hue: number): [number, number, number] {
126
101
  }
127
102
  }
128
103
 
129
- /**
130
- * `TH_VERTEX_UNREFERENCED`, the colour a zero-weight vertex is painted
131
- * (`overlay_paint_weight_frag.glsl:98-100`, `mix(weight_color,
132
- * color_unreferenced, alert * alert)` at alert 1).
133
- *
134
- * MEASURED: `vertex_unreferenced` appears NOWHERE in
135
- * `release/datafiles/userdef/userdef_default_theme.c` — the only mention of
136
- * the member in the whole checkout is its RNA declaration
137
- * (`rna_userdef.cc:3104`). The default theme therefore leaves the struct's
138
- * zero standing, which is BLACK, and black is what Blender draws for an
139
- * unweighted vertex. Stated rather than eyedropped.
140
- */
104
+ /** TH_VERTEX_UNREFERENCED is black: Blender’s default theme leaves that member zero. */
141
105
  export const VERTEX_UNREFERENCED = 0x000000;
142
106
 
143
- function bytesFrom(base64: string): Uint8Array {
144
- const binary = atob(base64);
145
- const bytes = new Uint8Array(binary.length);
146
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
147
- return bytes;
148
- }
149
-
150
107
  /**
151
108
  * THE OVERLAY: the painted object's own geometry, drawn again in the weight
152
109
  * colours.
@@ -173,34 +130,14 @@ export class WeightOverlay {
173
130
  this.group.name = 'BlenderWeightOverlay';
174
131
  }
175
132
 
176
- /**
177
- * Draw the weights over `source`, the presented mesh of the painted object.
178
- * `null` weights (no active vertex group, no active mesh) clears it.
179
- *
180
- * `objectMatrix` is the object's matrix in BLENDER's frame — the frame's own
181
- * `objects[].matrix`, exactly as the armature overlay takes it, and NOT the
182
- * presented mesh's `matrixWorld`. Measured live 2026-09-19: `matrixWorld`
183
- * already carries the model root's Z-up permutation, and this group carries
184
- * it too, so the drawing landed with the permutation applied TWICE — the
185
- * Body's colours lay flat on the floor beside the standing cylinder.
186
- *
187
- * Returns the warning this drawing could not honour, or null.
188
- */
133
+ /** Draw active-group weights over source, or clear the drawing for null weights. objectMatrix is objects[].matrix in Blender coordinates, before the model root’s Z-up conversion. Return an unsupported-drawing warning or null. */
189
134
  apply(
190
135
  weights: BlenderWeights | null,
191
136
  source: THREE.Mesh | null,
192
137
  objectMatrix: THREE.Matrix4 | null,
193
138
  ): string | null {
194
139
  if (weights === null || source === null || !source.isMesh || objectMatrix === null) {
195
- // THE DRAWING GOES, THE ARRAY STAYS. Dropping `held` here is what the
196
- // walk caught (2026-09-19): `_weights` answers null whenever the active
197
- // object is not a mesh — entering POSE MODE on the rig is enough — while
198
- // the session's `_known` still records the array as sent, so the next
199
- // frame that names the Body again ships a REFERENCE to bytes this side
200
- // had just thrown away, and the colours never came back. The array is
201
- // still the truth about that object's group; only the drawing is stale.
202
- // It is the same rule the meshes follow: a presenter holds its geometry
203
- // until the SESSION replaces it, never until a frame stops mentioning it.
140
+ // Clear the drawing while retaining received weight arrays. The session may refer to those bytes again until it replaces them.
204
141
  this.clear();
205
142
  this.signature = '';
206
143
  return null;
@@ -222,11 +159,11 @@ export class WeightOverlay {
222
159
  this.clear();
223
160
  return `weights: ${key} carried neither an array nor a reference`;
224
161
  }
225
- const raw = bytesFrom(weights.weightsBase64);
162
+ const raw = bytesFromBase64(weights.weightsBase64);
226
163
  this.held = {
227
164
  key,
228
165
  weights: new Float32Array(raw.buffer, raw.byteOffset, weights.count),
229
- alerts: bytesFrom(weights.alertBase64),
166
+ alerts: bytesFromBase64(weights.alertBase64),
230
167
  };
231
168
  }
232
169
  const held = this.held;
@@ -1,19 +1,4 @@
1
- /**
2
- * Blender's scattering skies, transcribed from the pinned source.
3
- *
4
- * `intern/sky/source/sky_multiple_scattering.cpp` precomputes a 512x256 XYZ
5
- * texture and `gpu_shader_material_tex_sky.glsl`'s `node_tex_sky_nishita` reads
6
- * it back per direction; both halves are here because both are what a script
7
- * sees through `ShaderNodeTexSky`. The single-scattering atmosphere is in
8
- * `blender-single-scattering-sky.ts`; model selection, caching and the shared
9
- * GPU lookup convention live here. Each model keeps its own native elevation
10
- * and azimuth conventions.
11
- *
12
- * The constants and the arithmetic are the source's. The XYZ-to-scene-linear
13
- * matrix is not in the source -- it comes from the colour configuration -- and
14
- * was measured off the oracle by round-tripping an EXR through
15
- * `Linear CIE-XYZ D65`.
16
- */
1
+ /** Blender scattering skies from the pinned source. Multiple scattering uses a precomputed XYZ texture; single scattering lives in blender-single-scattering-sky.ts. This module selects models, caches results and converts XYZ into scene-linear colour. */
17
2
  import * as THREE from 'three';
18
3
  import {precomputeSingleScatteringSky, precomputeSingleScatteringSun} from './blender-single-scattering-sky';
19
4
 
@@ -53,8 +38,7 @@ const SPECTRAL_XYZ = [
53
38
  [20.762668673810577145, 2.3614213523314368527, 110.48009643252140334],
54
39
  ];
55
40
 
56
- /** `IMB_colormanagement_get_xyz_to_scene_linear()`, measured on the oracle by
57
- * loading an EXR of the three basis vectors as `Linear CIE-XYZ D65`. */
41
+ /** XYZ-to-scene-linear matrix from EXR basis-vector conversion through Linear CIE-XYZ D65. */
58
42
  const XYZ_TO_RGB = [
59
43
  [3.2409698963165283, -1.5373831987380981, -0.49861079454421997],
60
44
  [-0.969243586063385, 1.8759675025939941, 0.041555099189281464],
@@ -102,11 +86,11 @@ const aerosolDensity = (h: number): number =>
102
86
  (Math.exp(-h / AEROSOL_HEIGHT_SCALE) + AEROSOL_BACKGROUND_DENSITY / AEROSOL_BASE_DENSITY);
103
87
 
104
88
  export interface SkyParameters {
105
- /** Omitted in older frames, whose only supported model was multiple scattering. */
106
- model?: 'SINGLE_SCATTERING' | 'MULTIPLE_SCATTERING';
107
- sunDisc?: boolean | undefined;
108
- sunSize?: number | undefined;
109
- sunIntensity?: number | undefined;
89
+ /** Sky model selected by the frame. */
90
+ model: 'SINGLE_SCATTERING' | 'MULTIPLE_SCATTERING';
91
+ sunDisc: boolean;
92
+ sunSize: number;
93
+ sunIntensity: number;
110
94
  sunElevation: number;
111
95
  sunRotation: number;
112
96
  altitude: number;
@@ -357,13 +341,9 @@ function precomputeTexture(p: SkyParameters, sunElevation: number): Float32Array
357
341
  }
358
342
 
359
343
  /**
360
- * THE 15-SECOND HALF, as a pure function of the parameters.
361
- *
362
- * `precomputeTexture` walks 512x256 texels with 64 in-scattering steps each and
363
- * that is where a sky's cost lives; reading it back (below) is 32,768 bilinear
364
- * lookups and costs milliseconds. Split out because the expensive half takes
365
- * numbers and returns numbers -- no THREE, no DOM -- so a WORKER can run it and
366
- * post the Float32Array back. See `contributions/sky-precompute-worker.ts`.
344
+ * Precompute sky radiance from numeric parameters without Three or DOM access.
345
+ * `sky-precompute-worker.ts` runs this work off the main thread
346
+ * and returns the Float32Array for synchronous field sampling.
367
347
  */
368
348
  export function precomputeSkyTexture(p: SkyParameters): Float32Array {
369
349
  if (p.model === 'SINGLE_SCATTERING') return precomputeSingleScatteringSky(p);
@@ -376,7 +356,7 @@ export function precomputeSkyTexture(p: SkyParameters): Float32Array {
376
356
  * single scattering keeps it as supplied to Blender's precompute. */
377
357
  export function skyTextureKey(p: SkyParameters): string {
378
358
  const elevation = p.model === 'SINGLE_SCATTERING' ? p.sunElevation : simplify(p.sunElevation, p.sunRotation)[0];
379
- return JSON.stringify([p.model ?? 'MULTIPLE_SCATTERING', elevation, p.altitude, p.airDensity, p.aerosolDensity, p.ozoneDensity]);
359
+ return JSON.stringify([p.model, elevation, p.altitude, p.airDensity, p.aerosolDensity, p.ozoneDensity]);
380
360
  }
381
361
 
382
362
  /** Precomputed sky textures, however they were derived. `skyField` is
@@ -433,7 +413,7 @@ export function skyField(p: SkyParameters): (direction: THREE.Vector3) => THREE.
433
413
  primeSkyTexture(p, pixels);
434
414
  }
435
415
  const colour = new THREE.Vector3();
436
- const diameter = p.sunSize ?? 0;
416
+ const diameter = p.sunSize;
437
417
  const solar = p.model === 'SINGLE_SCATTERING' && p.sunDisc && diameter > 0
438
418
  ? precomputeSingleScatteringSun(p, diameter) : null;
439
419
  const sun = skySunDirection(p);
@@ -478,7 +458,7 @@ export function skyField(p: SkyParameters): (direction: THREE.Vector3) => THREE.
478
458
  if (angle < diameter / 2) {
479
459
  const limb = 1 - .6 * (1 - Math.sqrt(Math.max(0, 1 - (angle / (diameter / 2)) ** 2)));
480
460
  const t = (dirElevation - p.sunElevation) / diameter + .5;
481
- for (let i = 0; i < 3; i++) xyz[i]! += mix(solar[0][i]!, solar[1][i]!, t) * (p.sunIntensity ?? 1) * limb;
461
+ for (let i = 0; i < 3; i++) xyz[i]! += mix(solar[0][i]!, solar[1][i]!, t) * p.sunIntensity * limb;
482
462
  }
483
463
  }
484
464
  return colour.set(
@@ -1,33 +1,7 @@
1
1
  /**
2
- * `triangulate` — how a BMesh FACE becomes triangles, and the one place that
3
- * decision is made.
4
- *
5
- * A `BMesh` face is an n-gon; a `BufferGeometry` is triangles. The conversion
6
- * used to be a naive fan (`0,1,2`, `0,2,3`, `0,3,4`, …), which is correct for
7
- * a CONVEX polygon and silently wrong for a concave one: the fan's later
8
- * triangles fold back over the polygon's own reflex corner, so they overlap
9
- * each other and face BACKWARDS. That renders as a hole. It shipped as one —
10
- * the treasure chest's crescent lid end-cap (docs/BLENDER-PARITY.md §Driver
11
- * ladder, "Chest-measured gaps") — and, worse, `validate()` had nothing to say
12
- * about it, because every count and every winding on the BMesh side was
13
- * perfectly sound. The defect only existed in the export.
14
- *
15
- * So the export's triangulation is now a real one, and it is the SAME
16
- * function `validate` checks, which is what closes the gap: there is no way
17
- * for the exporter to fan something the validator did not look at.
18
- *
19
- * THE RUNG. Ear clipping is not hand-rolled here: `THREE.ShapeUtils
20
- * .triangulateShape` is three's own Earcut and ships in the dependency the
21
- * kit already has. This module supplies only the two things Earcut cannot do
22
- * for a 3D face — pick the plane to flatten onto, and decide whether the
23
- * result is trustworthy.
24
- *
25
- * THE FAN IS STILL THE FAST PATH, and deliberately so. Every face is fanned
26
- * FIRST and the fan is kept whenever it is sound, so every convex face — which
27
- * is nearly all of them, quads included — exports with byte-identical indices
28
- * to before this module existed. Earcut runs only where the fan actually
29
- * fails. That is not an optimization: it is what lets an existing model be
30
- * rebuilt through the fixed kit and change ONLY where it was broken.
2
+ * Triangulate BMesh n-gons for export and validation. Keep a fan when it covers the polygon without
3
+ * overlap or reversed triangles; otherwise flatten onto a suitable plane and use three’s Earcut.
4
+ * Both paths validate the resulting triangles against the face.
31
5
  */
32
6
 
33
7
  import * as THREE from 'three';
@@ -52,8 +26,8 @@ export interface PolygonTriangulation {
52
26
  * no flattening of it is simple, and neither the fan nor Earcut can fix
53
27
  * that. `validate` reports it as `overlappingTriangulation`. */
54
28
  readonly sound: boolean;
55
- /** Which path produced `triangles` — `'fan'` is the untouched legacy
56
- * layout, `'earcut'` is `THREE.ShapeUtils.triangulateShape`. */
29
+ /** Which path produced `triangles` — `'fan'` is the fan from the first
30
+ * corner, `'earcut'` is `THREE.ShapeUtils.triangulateShape`. */
57
31
  readonly method: 'fan' | 'earcut';
58
32
  }
59
33
 
@@ -64,8 +38,7 @@ export interface PolygonTriangulation {
64
38
  const AREA_TOLERANCE = 1e-6;
65
39
 
66
40
  /** The polygon's Newell area-weighted normal — the standard best-fit plane
67
- * for a possibly non-planar face, and the one whose length is twice the
68
- * projected area. */
41
+ * for a possibly non-planar face, scaled so its length is the projected area. */
69
42
  function newellNormal(points: readonly THREE.Vector3[]): THREE.Vector3 {
70
43
  const n = new THREE.Vector3();
71
44
  for (let i = 0; i < points.length; i++) {
@@ -82,8 +55,7 @@ function newellNormal(points: readonly THREE.Vector3[]): THREE.Vector3 {
82
55
  const cross2 = (a: THREE.Vector2, b: THREE.Vector2, c: THREE.Vector2): number =>
83
56
  (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
84
57
 
85
- /** The consecutive fan from corner 0 — the layout every export used before
86
- * this module, reproduced exactly so a sound fan stays byte-identical. */
58
+ /** Consecutive triangle fan from corner 0. */
87
59
  const fanOf = (n: number): TriangleIndices[] =>
88
60
  Array.from({ length: n - 2 }, (_, i) => [0, i + 1, i + 2] as const);
89
61
 
@@ -0,0 +1,235 @@
1
+ /** Viewport anti-aliasing from Blender 5.2: Workbench uses its jitter table and Blackman-Harris resolve; EEVEE uses Halton jitter and Gaussian film accumulation. The luma weight is constant at exposureScale 0. Photographs cap samples at PHOTOGRAPH_SAMPLE_MAX. */
2
+
3
+ /** The most samples a photograph draws. Workbench's own maximum is 32; EEVEE's viewport can be
4
+ * set past it, and 0 means it never stops. */
5
+ export const PHOTOGRAPH_SAMPLE_MAX = 64;
6
+
7
+ /** What the stage's `PhotographSampling` reads (`@volter/editor-threejs/capture/photograph-sampling`). */
8
+ export interface BlenderPhotographSampling {
9
+ readonly offsets: readonly (readonly [number, number])[];
10
+ readonly projectionScale: number;
11
+ readonly reach: number;
12
+ readonly filter: 'blackman-harris' | 'gaussian';
13
+ readonly radius: number;
14
+ readonly accumulation: 'log2' | 'luma';
15
+ readonly exposureScale: number;
16
+ readonly backdropMerge: boolean;
17
+ /** Show Workbench accumulation from sample 1 and EEVEE accumulation from sample 2, where their measured edges improve on the ordinary MSAA frame. */
18
+ readonly liveFrom: number;
19
+ }
20
+
21
+ const f32 = Math.fround;
22
+
23
+ /** `blender::RandomNumberGenerator` (drand48's constants), as `BLI_rng_new(seed)` seeds it. */
24
+ class BlenderRng {
25
+ private x: bigint;
26
+ constructor(seed: number) {
27
+ this.x = (BigInt(seed >>> 0) << 16n) | 0x330en;
28
+ }
29
+ private step(): void {
30
+ this.x = (0x5deece66dn * this.x + 0xbn) & 0xffffffffffffn;
31
+ }
32
+ getDouble(): number {
33
+ this.step();
34
+ return Number(BigInt.asIntN(32, this.x >> 17n)) / 0x80000000;
35
+ }
36
+ }
37
+
38
+ function jitterate1(jit1: [number, number][], jit2: [number, number][], radius1: number): void {
39
+ const num = jit1.length;
40
+ for (let i = num - 1; i >= 0; i--) {
41
+ let dvecx = 0;
42
+ let dvecy = 0;
43
+ const x0 = jit1[i]![0];
44
+ const y0 = jit1[i]![1];
45
+ for (let j = num - 1; j >= 0; j--) {
46
+ if (i === j) continue;
47
+ // Single-precision, rounded after each operation as C's float arithmetic is.
48
+ let vecx = f32(f32(jit1[j]![0] - x0) - 1);
49
+ let vecy = f32(f32(jit1[j]![1] - y0) - 1);
50
+ for (let k = 3; k > 0; k--) {
51
+ for (const step of [1, 1, -2]) {
52
+ if (Math.abs(vecx) < radius1 && Math.abs(vecy) < radius1) {
53
+ let len = f32(Math.sqrt(f32(f32(vecx * vecx) + f32(vecy * vecy))));
54
+ if (len > 0 && len < radius1) {
55
+ len = f32(len / radius1);
56
+ dvecx = f32(dvecx + f32(vecx / len));
57
+ dvecy = f32(dvecy + f32(vecy / len));
58
+ }
59
+ }
60
+ vecx = f32(vecx + step);
61
+ }
62
+ vecy = f32(vecy + 1);
63
+ }
64
+ }
65
+ let x = f32(x0 - f32(dvecx / 18));
66
+ let y = f32(y0 - f32(dvecy / 18));
67
+ x = f32(x - Math.floor(x));
68
+ y = f32(y - Math.floor(y));
69
+ jit2[i] = [x, y];
70
+ }
71
+ for (let i = 0; i < num; i++) jit1[i] = [jit2[i]![0], jit2[i]![1]];
72
+ }
73
+
74
+ function jitterate2(jit1: [number, number][], jit2: [number, number][], radius2: number): void {
75
+ const num = jit1.length;
76
+ for (let i = num - 1; i >= 0; i--) {
77
+ let dvecx = 0;
78
+ let dvecy = 0;
79
+ const x0 = jit1[i]![0];
80
+ const y0 = jit1[i]![1];
81
+ for (let j = num - 1; j >= 0; j--) {
82
+ if (i === j) continue;
83
+ // Single-precision, rounded after each operation as C's float arithmetic is.
84
+ let vecx = f32(f32(jit1[j]![0] - x0) - 1);
85
+ let vecy = f32(f32(jit1[j]![1] - y0) - 1);
86
+ for (let k = 0; k < 3; k++) {
87
+ if (Math.abs(vecx) < radius2) dvecx = f32(dvecx + f32(vecx * radius2));
88
+ vecx = f32(vecx + 1);
89
+ }
90
+ for (let k = 0; k < 3; k++) {
91
+ if (Math.abs(vecy) < radius2) dvecy = f32(dvecy + f32(vecy * radius2));
92
+ vecy = f32(vecy + 1);
93
+ }
94
+ }
95
+ let x = f32(x0 - f32(dvecx / 2));
96
+ let y = f32(y0 - f32(dvecy / 2));
97
+ x = f32(x - Math.floor(x));
98
+ y = f32(y - Math.floor(y));
99
+ jit2[i] = [x, y];
100
+ }
101
+ for (let i = 0; i < num; i++) jit1[i] = [jit2[i]![0], jit2[i]![1]];
102
+ }
103
+
104
+ /** `BLI_jitter_init`: `num` points in the unit square, centred on (0, 0). */
105
+ function bliJitterInit(num: number): [number, number][] {
106
+ const numFl = f32(num);
107
+ const numFlSqrt = f32(Math.sqrt(numFl));
108
+ const rad1 = f32(1 / numFlSqrt);
109
+ const rad2 = f32(1 / numFl);
110
+ const rad3 = f32(numFlSqrt / numFl);
111
+ const rng = new BlenderRng(31415926 + num);
112
+ const jit: [number, number][] = [];
113
+ let x = 0;
114
+ for (let i = 0; i < num; i++) {
115
+ const a = f32(x + f32(rad1 * f32(0.5 - rng.getDouble())));
116
+ const b = f32(f32(f32(i) / numFl) + f32(rad1 * f32(0.5 - rng.getDouble())));
117
+ jit.push([a, b]);
118
+ x = f32(x + rad3);
119
+ x = f32(x - Math.floor(x));
120
+ }
121
+ const jit2: [number, number][] = jit.map(() => [0, 0]);
122
+ for (let i = 0; i < 24; i++) {
123
+ jitterate1(jit, jit2, rad1);
124
+ jitterate1(jit, jit2, rad1);
125
+ jitterate2(jit, jit2, rad2);
126
+ }
127
+ return jit.map(([a, b]) => [f32(a - 0.5), f32(b - 0.5)]);
128
+ }
129
+
130
+ /** C's `fmodf`: the remainder carries the dividend's sign. */
131
+ const fmod = (a: number, b: number) => a - b * Math.trunc(a / b);
132
+
133
+ /** `TaaSamples::init_samples`: one of Workbench's tables, in [-1, 1) pixels-as-weighed. */
134
+ function workbenchTable(num: number): (readonly [number, number])[] {
135
+ const samples = bliJitterInit(num);
136
+ let closest = 0;
137
+ let closestDistance = 1;
138
+ samples.forEach(([x, y], i) => {
139
+ const d = f32(f32(x * x) + f32(y * y));
140
+ if (d < closestDistance) {
141
+ closestDistance = d;
142
+ closest = i;
143
+ }
144
+ });
145
+ const [cx, cy] = samples[closest]!;
146
+ const moved = samples.map(([x, y]): [number, number] => {
147
+ const sx = f32(x - cx);
148
+ const sy = f32(y - cy);
149
+ return [f32(f32(fmod(f32(sx + 0.5), 1) * 2) - 1), f32(f32(fmod(f32(sy + 0.5), 1) * 2) - 1)];
150
+ });
151
+ if (closest !== 0) [moved[0], moved[closest]] = [moved[closest]!, moved[0]!];
152
+ for (let i = 0; i < moved.length - 2; i++) {
153
+ let farthest = 0;
154
+ let index = i;
155
+ for (let j = i + 1; j < moved.length; j++) {
156
+ const dx = moved[i]![0] - moved[j]![0];
157
+ const dy = moved[i]![1] - moved[j]![1];
158
+ const d = dx * dx + dy * dy;
159
+ if (d > farthest) {
160
+ farthest = d;
161
+ index = j;
162
+ }
163
+ }
164
+ [moved[i + 1], moved[index]] = [moved[index]!, moved[i + 1]!];
165
+ }
166
+ return moved;
167
+ }
168
+
169
+ const workbenchTables = new Map<number, (readonly [number, number])[]>();
170
+
171
+ /** Workbench's sampling for a sample count (`viewport_aa` / `render_aa` as a number: 0 off,
172
+ * 1 the single sample its SMAA resolves alone, else 5, 8, 11, 16 or 32). Off is one centred
173
+ * sample kept as it is: no accumulation, no filter (`workbench_state.cc`, `draw_aa` false). */
174
+ export function workbenchSampling(count: number, backdropMerge: boolean): BlenderPhotographSampling {
175
+ if (count <= 0) {
176
+ return {
177
+ offsets: [[0, 0]], projectionScale: 0.5, reach: 0, filter: 'blackman-harris', radius: 0,
178
+ accumulation: 'log2', exposureScale: 1, backdropMerge, liveFrom: 1,
179
+ };
180
+ }
181
+ // One sample is the centre (`TaaSamples` always starts there); Workbench then SMAAs it.
182
+ const size = count <= 1 ? 1 : count <= 5 ? 5 : count <= 8 ? 8 : count <= 11 ? 11 : count <= 16 ? 16 : 32;
183
+ let offsets = workbenchTables.get(size);
184
+ if (!offsets) {
185
+ offsets = size === 1 ? [[0, 0]] : workbenchTable(size);
186
+ workbenchTables.set(size, offsets);
187
+ }
188
+ return {
189
+ offsets, projectionScale: 0.5, reach: 1, filter: 'blackman-harris', radius: 1,
190
+ accumulation: 'log2', exposureScale: 1, backdropMerge, liveFrom: 1,
191
+ };
192
+ }
193
+
194
+ /** `Sampling::sample_disk`. */
195
+ function sampleDisk(u: number, v: number): [number, number] {
196
+ const omega = v * 2 * Math.PI;
197
+ const r = Math.sqrt(u);
198
+ return [r * Math.cos(omega), r * Math.sin(omega)];
199
+ }
200
+
201
+ /** Radical inverse in `base` (`BLI_halton_2d` iterates to the same value for index `n`). */
202
+ function halton(n: number, base: number): number {
203
+ let result = 0;
204
+ let fraction = 1 / base;
205
+ for (let i = n; i > 0; i = Math.floor(i / base)) {
206
+ result += (i % base) * fraction;
207
+ fraction /= base;
208
+ }
209
+ return result;
210
+ }
211
+
212
+ /** EEVEE's film sampling over its viewport `taa_samples` (0: never stops) and `filter_size`. */
213
+ export function eeveeSampling(taaSamples: number, filterSize: number): BlenderPhotographSampling {
214
+ const count = Math.min(PHOTOGRAPH_SAMPLE_MAX, taaSamples > 0 ? taaSamples : PHOTOGRAPH_SAMPLE_MAX);
215
+ // A single sample is not filtered (`Film::init`), nor is a filter under 0.01, whose one texel
216
+ // is taken whole (`update_sample_table`).
217
+ const clamped = Math.min(100, Math.max(0, filterSize));
218
+ const radius = count === 1 || clamped < 0.01 ? 0 : clamped;
219
+ const fract = (x: number) => x - Math.floor(x);
220
+ const offsets: (readonly [number, number])[] = [];
221
+ for (let s = 0; s < count; s++) {
222
+ const u = fract(f32(halton(s + 1, 2) + f32(1 / 2)));
223
+ const v = fract(f32(halton(s + 1, 3) + f32(2 / 3)));
224
+ // A filter under 1/sqrt(2) (0 included) jitters over a disk of half its radius (`pixel_jitter_get`).
225
+ if (radius < Math.SQRT1_2) {
226
+ const [x, y] = sampleDisk(u, v);
227
+ offsets.push([x * 0.5 * radius, y * 0.5 * radius]);
228
+ } else offsets.push([u - 0.5, v - 0.5]);
229
+ }
230
+ return {
231
+ offsets, projectionScale: 1, reach: Math.min(3, Math.ceil(radius)), filter: 'gaussian', radius,
232
+ // Exposure scale zero gives a constant luma weight.
233
+ accumulation: 'luma', exposureScale: 0, backdropMerge: false, liveFrom: Math.min(2, count),
234
+ };
235
+ }
@@ -1,26 +1,4 @@
1
- /**
2
- * BLENDER'S SOLID SHADING, as Blender computes it: `get_world_lighting` from
3
- * `draw/engines/workbench/shaders/workbench_world_light_lib.glsl`, transcribed line for line,
4
- * over the four studio lights of the factory `Default` (`SOLID_LIGHTS`, read back from Blender
5
- * 5.2's preferences with their specular colours) and no ambient (`light_ambient` is zero).
6
- *
7
- * Per light: a wrapped diffuse term, `(N·L + wrap) / (1 + wrap)²`; a normalized Blinn specular
8
- * whose gloss the wrap softens, blended toward a wrapped "environment" term by `wrap²`; a
9
- * Fresnel lift of the specular colour (`brdf_approx`); and energy conservation that takes the
10
- * specular's grey level off the diffuse. The surface is the material's VIEWPORT DISPLAY
11
- * (`Material.diffuse_color`, `roughness`, `metallic`), which is what Solid's MATERIAL colour type
12
- * reads — never its node graph.
13
- *
14
- * WHY A FUNCTION AND NOT A LIGHT RIG: three's lambert and BRDF cannot express the wrap, the
15
- * coupled specular or the per-light specular colour, so the four lights plus one fitted gain
16
- * could match the default view's three faces only by absorbing the difference, and missed a
17
- * face square to the view by 18 levels (143 against Blender's 161). This function, evaluated
18
- * offline for that face, gives 161.
19
- *
20
- * Built on `MeshLambertMaterial` so skinning, morphs and the view-space normal come with it;
21
- * only the final colour is replaced. Blender uses a fast reciprocal (`fast_rcp`); this uses the
22
- * exact one.
23
- */
1
+ /** Blender Solid shading from workbench_world_light_lib.glsl with the factory studio lights and viewport display colours. Pack roughness as sqrt(Material.roughness), use Blender’s fast_rcp and respect the Specular Lighting toggle. MeshLambertMaterial supplies skinning, morphs and view-space normals. */
24
2
  import * as THREE from 'three';
25
3
  import { SOLID_LIGHTS } from './blender-runtime-lighting';
26
4
 
@@ -31,8 +9,12 @@ export interface ViewportDisplay {
31
9
  readonly metallic: number;
32
10
  }
33
11
 
34
- /** For a surface with no material: the factory cube's material's display, read back from Blender
35
- * 5.2 (grey 0.8, roughness 0.5, metallic 0). Blender's own default surface was not measured. */
12
+ /** `Material::pack_data`'s remap of the display roughness to Disney roughness. */
13
+ export function workbenchRoughness(display: ViewportDisplay): number {
14
+ return Math.sqrt(Math.min(1, Math.max(0, display.roughness)));
15
+ }
16
+
17
+ /** Default material display values: grey 0.8, roughness 0.5, metallic 0. */
36
18
  export const DEFAULT_VIEWPORT_DISPLAY: ViewportDisplay = { color: [0.8, 0.8, 0.8, 1], roughness: 0.5, metallic: 0 };
37
19
 
38
20
  const WORKBENCH_GLSL = /* glsl */ `
@@ -42,13 +24,25 @@ uniform vec3 wbSpecular[4];
42
24
  uniform vec4 wbWrap;
43
25
  uniform float wbRoughness;
44
26
  uniform float wbMetallic;
27
+ uniform bool wbSpecularOn;
28
+
29
+ // [Drobot2014a] as Blender has it (\`workbench_world_light_lib.glsl\`): an approximate reciprocal.
30
+ vec4 wbFastRcp(vec4 v) {
31
+ return intBitsToFloat(0x7eef370b - floatBitsToInt(v));
32
+ }
45
33
 
46
34
  vec4 wbWrapped(vec4 NL, vec4 w) {
47
35
  vec4 w1 = w + 1.0;
48
- return clamp((NL + w) / (w1 * w1), 0.0, 1.0);
36
+ return clamp((NL + w) * wbFastRcp(w1 * w1), 0.0, 1.0);
49
37
  }
50
38
 
51
39
  vec3 volterWorkbenchLighting(vec3 baseColor, vec3 N, vec3 I) {
40
+ if (!wbSpecularOn) {
41
+ vec4 plainNL = vec4(dot(wbDir[0], N), dot(wbDir[1], N), dot(wbDir[2], N), dot(wbDir[3], N));
42
+ vec4 plainLight = wbWrapped(plainNL, wbWrap);
43
+ return (plainLight.x * wbDiffuse[0] + plainLight.y * wbDiffuse[1] +
44
+ plainLight.z * wbDiffuse[2] + plainLight.w * wbDiffuse[3]) * baseColor;
45
+ }
52
46
  vec3 diffuseColor = mix(baseColor, vec3(0.0), wbMetallic);
53
47
  vec3 specularColor = mix(vec3(0.05), baseColor, wbMetallic);
54
48
  vec3 R = -reflect(I, N);
@@ -89,8 +83,9 @@ const LIGHT_DIFFUSE = vec3s((light) => light.diffuse);
89
83
  const LIGHT_SPECULAR = vec3s((light) => light.specular);
90
84
  const LIGHT_WRAP = new THREE.Vector4(...SOLID_LIGHTS.map((light) => light.smooth));
91
85
 
92
- /** One Solid-shaded surface: Blender's function over this viewport display. */
93
- export function workbenchMaterial(display: ViewportDisplay, side: THREE.Side): THREE.MeshLambertMaterial {
86
+ /** One Solid-shaded surface: Blender's function over this viewport display, with or without the
87
+ * specular term (`View3DShading.show_specular_highlight`). */
88
+ export function workbenchMaterial(display: ViewportDisplay, side: THREE.Side, specular = true): THREE.MeshLambertMaterial {
94
89
  const [r, g, b] = display.color;
95
90
  const material = new THREE.MeshLambertMaterial({ side });
96
91
  material.color.setRGB(r, g, b, THREE.LinearSRGBColorSpace);
@@ -100,8 +95,9 @@ export function workbenchMaterial(display: ViewportDisplay, side: THREE.Side): T
100
95
  shader.uniforms['wbDiffuse'] = { value: LIGHT_DIFFUSE };
101
96
  shader.uniforms['wbSpecular'] = { value: LIGHT_SPECULAR };
102
97
  shader.uniforms['wbWrap'] = { value: LIGHT_WRAP };
103
- shader.uniforms['wbRoughness'] = { value: display.roughness };
98
+ shader.uniforms['wbRoughness'] = { value: workbenchRoughness(display) };
104
99
  shader.uniforms['wbMetallic'] = { value: display.metallic };
100
+ shader.uniforms['wbSpecularOn'] = { value: specular };
105
101
  shader.fragmentShader = shader.fragmentShader
106
102
  .replace('void main() {', `${WORKBENCH_GLSL}\nvoid main() {`)
107
103
  .replace(