ohzi-core 14.0.0 → 15.0.0

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 (214) hide show
  1. package/README.md +2 -1
  2. package/build/index.mjs +1 -1
  3. package/build/index.mjs.map +1 -1
  4. package/package.json +8 -5
  5. package/src/Capabilities.ts +0 -2
  6. package/src/Debug.ts +38 -33
  7. package/src/Graphics.ts +2 -2
  8. package/src/Initializer.ts +2 -1
  9. package/src/SceneManager.ts +4 -5
  10. package/src/action_sequencer/NumberInterpolator.ts +2 -0
  11. package/src/blitter_index.ts +1 -2
  12. package/src/camera_controller/CameraController.ts +100 -59
  13. package/src/camera_controller/states/common/CommonCameraState.ts +2 -1
  14. package/src/components/AxisHelper.ts +1 -0
  15. package/src/components/Line.ts +35 -27
  16. package/src/components/PlaneHelper.ts +1 -1
  17. package/src/components/Text2D.ts +3 -1
  18. package/src/components/WorldImage.ts +27 -20
  19. package/src/components/sdf_text/SDFTextBatch.ts +16 -26
  20. package/src/dev_bridge/DebugDrawer.ts +399 -0
  21. package/src/index.ts +9 -12
  22. package/src/materials/AddMaterial.ts +14 -7
  23. package/src/materials/BlitNodeMaterial.ts +85 -0
  24. package/src/materials/BloomComposeMaterial.ts +21 -9
  25. package/src/materials/BoxBlurMaterial.ts +26 -6
  26. package/src/materials/ClearDepthNormalMaterial.ts +12 -6
  27. package/src/materials/DepthNormalMaterial.ts +25 -14
  28. package/src/materials/DualFilteringBlurMaterial.ts +32 -5
  29. package/src/materials/GaussianBlurMaterial.ts +36 -14
  30. package/src/materials/LineMaterial.ts +52 -0
  31. package/src/materials/LuminosityHighPassMaterial.ts +23 -10
  32. package/src/materials/NormalMaterial.ts +10 -11
  33. package/src/materials/SDFTextMaterial.ts +63 -16
  34. package/src/materials/SSAOComposeMaterial.ts +14 -5
  35. package/src/materials/SSAOMaterial.ts +123 -24
  36. package/src/materials/ScreenSpaceTextureMaterial.ts +19 -31
  37. package/src/materials/UnrealBloomComposeMaterial.ts +41 -32
  38. package/src/materials/WorldImageMaterial.ts +51 -0
  39. package/src/materials/deferred/DeferredPointLightMaterial.ts +45 -14
  40. package/src/materials/deferred/depth_normal_encoding.ts +36 -0
  41. package/src/primitives/Cube.ts +1 -1
  42. package/src/primitives/Sphere.ts +1 -1
  43. package/src/raycast/GroupRaycaster.ts +2 -2
  44. package/src/render_mode/BloomRender.ts +18 -4
  45. package/src/render_mode/DebugNormalsRender.ts +6 -1
  46. package/src/render_mode/DeferredRender.ts +5 -34
  47. package/src/render_mode/NormalAORender.ts +9 -19
  48. package/src/render_mode/UnrealBloomRender.ts +2 -1
  49. package/src/render_mode/VRRender.ts +6 -3
  50. package/src/render_utilities/Blitter.ts +4 -4
  51. package/src/render_utilities/Blurrer.ts +7 -0
  52. package/src/render_utilities/DualFilteringBlurrer.ts +26 -10
  53. package/src/resource_loader/AudioLoader.ts +5 -3
  54. package/src/resource_loader/HDRTextureLoader.ts +1 -1
  55. package/src/scenes/AbstractScene.ts +7 -3
  56. package/src/scenes/Compilator.ts +5 -1
  57. package/src/scenes/loading_states/LoadingState.ts +15 -7
  58. package/src/scenes/loading_states/RegularLoadingState.ts +2 -1
  59. package/src/utilities/CameraUtilities.ts +57 -79
  60. package/src/utilities/HTMLUtilities.ts +2 -1
  61. package/src/utilities/MeshSampler.ts +8 -1
  62. package/src/utilities/OrthographicFrustumPointFitter.ts +53 -4
  63. package/src/view_components/ApplicationView.ts +1 -1
  64. package/src/view_components/ViewComponent.ts +1 -1
  65. package/src/view_components/ViewManager.ts +7 -1
  66. package/src/view_components/transition/DrawIOAnimationSheet.ts +1 -1
  67. package/types/Capabilities.d.ts +0 -1
  68. package/types/SceneManager.d.ts +1 -1
  69. package/types/blitter_index.d.ts +1 -2
  70. package/types/camera_controller/CameraController.d.ts +9 -0
  71. package/types/components/Line.d.ts +3 -2
  72. package/types/components/WorldImage.d.ts +3 -2
  73. package/types/index.d.ts +2 -7
  74. package/types/materials/AddMaterial.d.ts +2 -2
  75. package/types/materials/BlitNodeMaterial.d.ts +18 -0
  76. package/types/materials/BloomComposeMaterial.d.ts +2 -2
  77. package/types/materials/BoxBlurMaterial.d.ts +2 -2
  78. package/types/materials/ClearDepthNormalMaterial.d.ts +2 -2
  79. package/types/materials/DepthNormalMaterial.d.ts +2 -2
  80. package/types/materials/DualFilteringBlurMaterial.d.ts +2 -2
  81. package/types/materials/GaussianBlurMaterial.d.ts +2 -2
  82. package/types/materials/LuminosityHighPassMaterial.d.ts +2 -2
  83. package/types/materials/NormalMaterial.d.ts +2 -2
  84. package/types/materials/SDFTextMaterial.d.ts +2 -2
  85. package/types/materials/SSAOComposeMaterial.d.ts +2 -2
  86. package/types/materials/SSAOMaterial.d.ts +2 -2
  87. package/types/materials/ScreenSpaceTextureMaterial.d.ts +2 -2
  88. package/types/materials/UnrealBloomComposeMaterial.d.ts +2 -2
  89. package/types/materials/deferred/DeferredPointLightMaterial.d.ts +2 -2
  90. package/types/render_mode/DeferredRender.d.ts +0 -2
  91. package/types/render_mode/NormalAORender.d.ts +0 -2
  92. package/types/render_utilities/Blitter.d.ts +2 -2
  93. package/types/render_utilities/DualFilteringBlurrer.d.ts +0 -2
  94. package/types/utilities/CameraUtilities.d.ts +0 -1
  95. package/types/utilities/OrthographicFrustumPointFitter.d.ts +14 -8
  96. package/build/index.module.js +0 -2
  97. package/build/index.module.js.map +0 -1
  98. package/src/components/EdgeMesh.ts +0 -150
  99. package/src/components/GeometryEdgeVisualizer.ts +0 -49
  100. package/src/components/MultiLinePath.ts +0 -25
  101. package/src/components/MultiLineText2D.ts +0 -65
  102. package/src/components/Shape2D.ts +0 -42
  103. package/src/components/Shape3D.ts +0 -38
  104. package/src/gpu_particles/GPUParticleSystem.ts +0 -122
  105. package/src/gpu_particles/ParticleAttribute.ts +0 -110
  106. package/src/gpu_particles/ParticlePositionAttribute.ts +0 -46
  107. package/src/materials/AlphaFilterMaterial.ts +0 -21
  108. package/src/materials/BaseShaderMaterial.ts +0 -20
  109. package/src/materials/BlitMaterial.ts +0 -27
  110. package/src/materials/DeferredRendererComposeMaterial.ts +0 -36
  111. package/src/materials/DisplayNormalTextureMaterial.ts +0 -18
  112. package/src/materials/FXAAMaterial.ts +0 -12
  113. package/src/materials/MedianFilterMaterial.ts +0 -46
  114. package/src/materials/SDFScreenTextMaterial.ts +0 -32
  115. package/src/materials/UnrealBlurMaterial.ts +0 -20
  116. package/src/materials/UnrealComposeMaterial.ts +0 -27
  117. package/src/materials/ViewPositionMaterial.ts +0 -19
  118. package/src/materials/gpu_particles/AttributeUpdateMaterial.ts +0 -35
  119. package/src/materials/gpu_particles/BasicParticleMaterial.ts +0 -25
  120. package/src/materials/gpu_particles/ParticleStorageMaterial.ts +0 -36
  121. package/src/materials/gpu_particles/PositionStorageMaterial.ts +0 -27
  122. package/src/render_utilities/MedianFilter.ts +0 -72
  123. package/src/render_utilities/ViewPositionRenderer.ts +0 -53
  124. package/src/shaders/add/add.frag +0 -28
  125. package/src/shaders/anti_aliasing/fxaa.frag +0 -72
  126. package/src/shaders/basic_color/basic_color.frag +0 -6
  127. package/src/shaders/basic_color/basic_color.vert +0 -10
  128. package/src/shaders/basic_line/basic_line.frag +0 -37
  129. package/src/shaders/basic_line/basic_line.vert +0 -37
  130. package/src/shaders/basic_texture/basic_texture.frag +0 -10
  131. package/src/shaders/basic_texture/basic_texture.vert +0 -17
  132. package/src/shaders/bloom/bloom_compose.frag +0 -22
  133. package/src/shaders/bloom/cheap_bloom_compose.frag +0 -13
  134. package/src/shaders/box_blur/box_blur.frag +0 -24
  135. package/src/shaders/box_blur/compose.frag +0 -31
  136. package/src/shaders/clear/clear_depth_normal.frag +0 -25
  137. package/src/shaders/copy/copy.frag +0 -7
  138. package/src/shaders/copy/copy.vert +0 -6
  139. package/src/shaders/deferred/blit_copy.vert +0 -11
  140. package/src/shaders/deferred/deferred_compose.frag +0 -59
  141. package/src/shaders/deferred/point_light.frag +0 -66
  142. package/src/shaders/deferred/point_light.vert +0 -13
  143. package/src/shaders/depth_normals/debug_normals.frag +0 -48
  144. package/src/shaders/depth_normals/depth_normals.frag +0 -30
  145. package/src/shaders/depth_normals/depth_normals.vert +0 -13
  146. package/src/shaders/dual_filter_blur/alpha_filter.frag +0 -14
  147. package/src/shaders/dual_filter_blur/downsample.frag +0 -21
  148. package/src/shaders/dual_filter_blur/upsample.frag +0 -24
  149. package/src/shaders/edge_visualizer/edge_visualizer.frag +0 -18
  150. package/src/shaders/edge_visualizer/edge_visualizer.vert +0 -16
  151. package/src/shaders/edges/corners.frag +0 -11
  152. package/src/shaders/edges/corners.vert +0 -31
  153. package/src/shaders/edges/edges.frag +0 -7
  154. package/src/shaders/edges/edges.vert +0 -27
  155. package/src/shaders/gaussian_blur/gaussian_blur.frag +0 -44
  156. package/src/shaders/gaussian_blur/unreal_bloom_compose.frag +0 -36
  157. package/src/shaders/gpu_particles/common_utils.glsl +0 -13
  158. package/src/shaders/gpu_particles/generic_storage.frag +0 -6
  159. package/src/shaders/gpu_particles/store/store_position.vert +0 -14
  160. package/src/shaders/gpu_particles/update/basic_update.frag +0 -21
  161. package/src/shaders/gpu_particles/visualize/visualize.frag +0 -11
  162. package/src/shaders/gpu_particles/visualize/visualize.vert +0 -12
  163. package/src/shaders/grid/grid.frag +0 -17
  164. package/src/shaders/grid/grid.vert +0 -10
  165. package/src/shaders/luminosity_high_pass/luminosity_high_pass.frag +0 -38
  166. package/src/shaders/median_filter/median_filter.frag +0 -134
  167. package/src/shaders/normal/normal.frag +0 -6
  168. package/src/shaders/normal/normal.vert +0 -6
  169. package/src/shaders/sdf_text/sdf_screen_text.frag +0 -51
  170. package/src/shaders/sdf_text/sdf_screen_text.vert +0 -42
  171. package/src/shaders/sdf_text/sdf_text.frag +0 -53
  172. package/src/shaders/sdf_text/sdf_text.vert +0 -42
  173. package/src/shaders/ssao/hbao.frag +0 -264
  174. package/src/shaders/ssao/ssao.frag +0 -95
  175. package/src/shaders/ssao/ssao.vert +0 -11
  176. package/src/shaders/ssao/ssao_compose.frag +0 -14
  177. package/src/shaders/transparent_mix/copy.frag +0 -7
  178. package/src/shaders/transparent_mix/transparent_mix.frag +0 -19
  179. package/src/shaders/transparent_mix/transparent_mix.vert +0 -8
  180. package/src/shaders/ui/ss_texture.frag +0 -11
  181. package/src/shaders/ui/ss_texture.vert +0 -32
  182. package/src/shaders/ui/ws_texture.vert +0 -39
  183. package/src/shaders/unreal_blur/unreal_blur.frag +0 -25
  184. package/src/shaders/unreal_blur/unreal_compose.frag +0 -24
  185. package/src/shaders/write_view_position/write_view_position.frag +0 -7
  186. package/src/shaders/write_view_position/write_view_position.vert +0 -9
  187. package/src/types.d.ts +0 -14
  188. package/types/components/EdgeMesh.d.ts +0 -16
  189. package/types/components/GeometryEdgeVisualizer.d.ts +0 -8
  190. package/types/components/MultiLinePath.d.ts +0 -8
  191. package/types/components/MultiLineText2D.d.ts +0 -12
  192. package/types/components/Shape2D.d.ts +0 -7
  193. package/types/components/Shape3D.d.ts +0 -6
  194. package/types/gpu_particles/GPUParticleSystem.d.ts +0 -19
  195. package/types/gpu_particles/ParticleAttribute.d.ts +0 -20
  196. package/types/gpu_particles/ParticlePositionAttribute.d.ts +0 -6
  197. package/types/materials/AlphaFilterMaterial.d.ts +0 -7
  198. package/types/materials/BaseShaderMaterial.d.ts +0 -4
  199. package/types/materials/BlitMaterial.d.ts +0 -4
  200. package/types/materials/DeferredRendererComposeMaterial.d.ts +0 -10
  201. package/types/materials/DisplayNormalTextureMaterial.d.ts +0 -4
  202. package/types/materials/FXAAMaterial.d.ts +0 -5
  203. package/types/materials/MedianFilterMaterial.d.ts +0 -8
  204. package/types/materials/SDFScreenTextMaterial.d.ts +0 -8
  205. package/types/materials/UIElementMaterial.d.ts +0 -4
  206. package/types/materials/UnrealBlurMaterial.d.ts +0 -5
  207. package/types/materials/UnrealComposeMaterial.d.ts +0 -5
  208. package/types/materials/ViewPositionMaterial.d.ts +0 -5
  209. package/types/materials/gpu_particles/AttributeUpdateMaterial.d.ts +0 -8
  210. package/types/materials/gpu_particles/BasicParticleMaterial.d.ts +0 -5
  211. package/types/materials/gpu_particles/ParticleStorageMaterial.d.ts +0 -5
  212. package/types/materials/gpu_particles/PositionStorageMaterial.d.ts +0 -5
  213. package/types/render_utilities/MedianFilter.d.ts +0 -12
  214. package/types/render_utilities/ViewPositionRenderer.d.ts +0 -12
@@ -1,36 +1,31 @@
1
- import line_fs from '../shaders/basic_line/basic_line.frag';
2
- import line_vs from '../shaders/basic_line/basic_line.vert';
1
+ import { LineMaterial } from '../materials/LineMaterial';
3
2
 
4
- import type { Vector3 } from 'three';
5
- import { BufferAttribute, BufferGeometry, Color, Mesh, ShaderMaterial } from 'three';
3
+ import type { Color, ColorRepresentation, Vector3 } from 'three';
4
+ import { BufferAttribute, BufferGeometry, Mesh } from 'three';
5
+
6
+ // An empty geometry with every attribute LineMaterial reads.
7
+ function create_line_geometry(): BufferGeometry
8
+ {
9
+ const geometry = new BufferGeometry();
10
+ geometry.setAttribute('position', new BufferAttribute(new Float32Array([]), 3));
11
+ geometry.setAttribute('next_position', new BufferAttribute(new Float32Array([]), 3));
12
+ geometry.setAttribute('previous_position', new BufferAttribute(new Float32Array([]), 3));
13
+ geometry.setAttribute('orientation', new BufferAttribute(new Float32Array([]), 1));
14
+ geometry.setAttribute('coverage', new BufferAttribute(new Float32Array([]), 1));
15
+
16
+ return geometry;
17
+ }
6
18
 
7
19
  class Line extends Mesh
8
20
  {
9
21
  _length: number;
10
22
  accumulated_length: number;
11
- material: ShaderMaterial
23
+ material: LineMaterial;
12
24
 
13
25
  constructor(points?: Vector3[])
14
26
  {
15
- const geometry = new BufferGeometry();
16
- geometry.setAttribute('position', new BufferAttribute(new Float32Array([]), 3));
17
- geometry.setAttribute('next_position', new BufferAttribute(new Float32Array([]), 3));
18
- geometry.setAttribute('previous_position', new BufferAttribute(new Float32Array([]), 3));
19
- geometry.setAttribute('orientation', new BufferAttribute(new Float32Array([]), 1));
20
- geometry.setAttribute('coverage', new BufferAttribute(new Float32Array([]), 1));
21
-
22
- const material = new ShaderMaterial({
23
- uniforms: {
24
- _Thickness: { value: 0.2 },
25
- _Length: { value: 0 },
26
- _ElapsedTime: { value: 0 },
27
- _Color: { value: new Color('#FF0000') }
28
- },
29
- vertexShader: line_vs,
30
- fragmentShader: line_fs,
31
- transparent: true,
32
- depthWrite: false
33
- });
27
+ const geometry = create_line_geometry();
28
+ const material = new LineMaterial();
34
29
 
35
30
  super(geometry, material);
36
31
  this.material = material;
@@ -106,7 +101,18 @@ class Line extends Mesh
106
101
  indices.push(index + 1);
107
102
  }
108
103
 
109
- this.geometry.setIndex(indices);
104
+ // WebGPURenderer sizes each GPU buffer at its first upload and later only writes
105
+ // into it, and it frees a geometry's buffers on its first dispose() only. So a new
106
+ // point count moves the line to a new geometry, which the next render uploads at
107
+ // the new size, and frees the old one. The index depends only on the point count,
108
+ // so it is kept otherwise.
109
+ if (vertexList.length / 3 !== this.geometry.getAttribute('position').count)
110
+ {
111
+ const previous_geometry = this.geometry;
112
+ this.geometry = create_line_geometry();
113
+ this.geometry.setIndex(indices);
114
+ previous_geometry.dispose();
115
+ }
110
116
  (this.geometry.getAttribute('position') as BufferAttribute).copy(new BufferAttribute(vertexList, 3));
111
117
  (this.geometry.getAttribute('next_position') as BufferAttribute).copy(new BufferAttribute(nextPositionList, 3));
112
118
  (this.geometry.getAttribute('previous_position') as BufferAttribute).copy(new BufferAttribute(previousPositionList, 3));
@@ -180,14 +186,16 @@ class Line extends Mesh
180
186
  {
181
187
  this.parent.remove(this);
182
188
  }
189
+
190
+ super.dispose();
183
191
  }
184
192
 
185
- set color(col)
193
+ set color(col: ColorRepresentation)
186
194
  {
187
195
  this.material.uniforms._Color.value.set(col);
188
196
  }
189
197
 
190
- get color()
198
+ get color(): Color
191
199
  {
192
200
  return this.material.uniforms._Color.value;
193
201
  }
@@ -27,7 +27,7 @@ class PlaneHelper extends Object3D
27
27
 
28
28
  dispose()
29
29
  {
30
-
30
+ super.dispose();
31
31
  }
32
32
  }
33
33
 
@@ -2,7 +2,7 @@ import { SimpleTextDrawer } from '../canvas_drawer/SimpleTextDrawer';
2
2
  import { WorldImage } from './WorldImage';
3
3
 
4
4
  import type { CanvasTexture, Vector2 } from 'three';
5
- import { LinearFilter, LinearMipMapLinearFilter } from 'three';
5
+ import { LinearFilter, LinearMipMapLinearFilter, SRGBColorSpace } from 'three';
6
6
 
7
7
  class Text2D extends WorldImage
8
8
  {
@@ -22,6 +22,8 @@ class Text2D extends WorldImage
22
22
 
23
23
  canvas_texture.minFilter = LinearMipMapLinearFilter;
24
24
  canvas_texture.minFilter = LinearFilter;
25
+ // Canvas colors are CSS colors, so sRGB. Tagged, they render as the given color.
26
+ canvas_texture.colorSpace = SRGBColorSpace;
25
27
  canvas_texture.needsUpdate = true;
26
28
  super(canvas_texture, pivot);
27
29
 
@@ -1,31 +1,20 @@
1
- import basic_texture_frag from '../shaders/basic_texture/basic_texture.frag';
2
- import basic_texture_vert from '../shaders/basic_texture/basic_texture.vert';
1
+ import { WorldImageMaterial } from '../materials/WorldImageMaterial';
3
2
 
4
3
  import type { Texture } from 'three';
5
- import { DoubleSide, Mesh, PlaneGeometry, ShaderMaterial, Vector2, Vector3 } from 'three';
4
+ import { Mesh, PlaneGeometry, Vector2, Vector3 } from 'three';
6
5
 
7
6
  class WorldImage extends Mesh
8
7
  {
9
8
  current_scale: number;
9
+ sized_texture: Texture;
10
+ texture_size: Vector2;
10
11
  tmp_bb_size: Vector3;
11
- material: ShaderMaterial;
12
+ material: WorldImageMaterial;
12
13
 
13
14
  constructor(texture: Texture, pivot: Vector2)
14
15
  {
15
16
  pivot = pivot || new Vector2(0, 0);
16
- const material = new ShaderMaterial({
17
- uniforms: {
18
- _MainTex: { value: texture },
19
- _ScreenAligned: { value: 0 },
20
- _Scale: { value: 1 },
21
- _Opacity: { value: 1 }
22
- },
23
- vertexShader: basic_texture_vert,
24
- fragmentShader: basic_texture_frag,
25
- transparent: true,
26
- depthWrite: false,
27
- side: DoubleSide
28
- });
17
+ const material = new WorldImageMaterial(texture);
29
18
  const geometry = new PlaneGeometry(1, 1, 1);
30
19
  geometry.translate(-pivot.x / 2, -pivot.y / 2, 0);
31
20
  // @ts-expect-error -- threejs issue
@@ -33,6 +22,9 @@ class WorldImage extends Mesh
33
22
  geometry.scale(current_scale, 1, 1);
34
23
  super(geometry, material);
35
24
  this.current_scale = current_scale;
25
+ this.sized_texture = texture;
26
+ // @ts-expect-error -- threejs issue
27
+ this.texture_size = new Vector2(texture.image.width, texture.image.height);
36
28
  this.geometry.computeBoundingBox();
37
29
 
38
30
  this.tmp_bb_size = new Vector3();
@@ -42,8 +34,21 @@ class WorldImage extends Mesh
42
34
 
43
35
  update_texture()
44
36
  {
45
- this.material.uniforms._MainTex.value.needsUpdate = true;
46
- const img = this.material.uniforms._MainTex.value.image;
37
+ const texture = this.material.uniforms._MainTex.value;
38
+ const img = texture.image as { width: number, height: number };
39
+
40
+ // WebGPURenderer allocates the GPU texture at the image size once and later only
41
+ // uploads into it, so an image that changed size needs a new one. A texture swapped
42
+ // into _MainTex may be shared, so it is only measured, never freed.
43
+ const resized = img.width !== this.texture_size.x || img.height !== this.texture_size.y;
44
+
45
+ if (texture === this.sized_texture && resized)
46
+ {
47
+ texture.dispose();
48
+ }
49
+ this.sized_texture = texture;
50
+ this.texture_size.set(img.width, img.height);
51
+ texture.needsUpdate = true;
47
52
 
48
53
  this.geometry.scale(1 / this.current_scale, 1, 1);
49
54
  this.current_scale = img.width / img.height;
@@ -60,7 +65,7 @@ class WorldImage extends Mesh
60
65
  set size(value)
61
66
  {
62
67
  this.scale.copy(value);
63
- this.material.uniforms._Scale.value = value;
68
+ this.material.uniforms._Scale.value.copy(value);
64
69
  }
65
70
 
66
71
  set screen_aligned(boolean)
@@ -89,6 +94,8 @@ class WorldImage extends Mesh
89
94
  this.parent.remove(this);
90
95
  this.material.uniforms._MainTex.value.dispose();
91
96
  this.material.dispose();
97
+
98
+ super.dispose();
92
99
  }
93
100
  }
94
101
 
@@ -5,13 +5,17 @@ import { SDFText } from './SDFText';
5
5
  import {
6
6
  DynamicDrawUsage,
7
7
  Float32BufferAttribute,
8
- InstancedBufferAttribute,
9
8
  InstancedBufferGeometry,
9
+ InstancedInterleavedBuffer,
10
+ InterleavedBufferAttribute,
10
11
  Mesh,
11
12
  PlaneGeometry,
12
13
  Vector2
13
14
  } from 'three';
14
15
 
16
+ // The per glyph attributes, four floats each, in the order they sit in an instance.
17
+ const GLYPH_ATTRIBUTES = ['transformsCol0', 'transformsCol1', 'transformsCol2', 'transformsCol3', 'glyph_bounds', 'plane_bounds', 'color'];
18
+
15
19
  class SDFTextBatch extends Mesh
16
20
  {
17
21
  font_layout: any;
@@ -33,32 +37,16 @@ class SDFTextBatch extends Mesh
33
37
  instanced_geometry.setAttribute('uv', new Float32BufferAttribute(geometry.getAttribute('uv').array, 2));
34
38
  instanced_geometry.index = geometry.index;
35
39
 
36
- const transformsCol0 = new InstancedBufferAttribute(new Float32Array(max_allocations * 4), 4, false);
37
- const transformsCol1 = new InstancedBufferAttribute(new Float32Array(max_allocations * 4), 4, false);
38
- const transformsCol2 = new InstancedBufferAttribute(new Float32Array(max_allocations * 4), 4, false);
39
- const transformsCol3 = new InstancedBufferAttribute(new Float32Array(max_allocations * 4), 4, false);
40
-
41
- const glyph_bounds = new InstancedBufferAttribute(new Float32Array(max_allocations * 4), 4, false);
42
- const plane_bounds = new InstancedBufferAttribute(new Float32Array(max_allocations * 4), 4, false);
43
-
44
- const color = new InstancedBufferAttribute(new Float32Array(max_allocations * 4), 4, false);
45
-
46
- transformsCol0.usage = DynamicDrawUsage;
47
- transformsCol1.usage = DynamicDrawUsage;
48
- transformsCol2.usage = DynamicDrawUsage;
49
- transformsCol3.usage = DynamicDrawUsage;
50
- glyph_bounds.usage = DynamicDrawUsage;
51
- plane_bounds.usage = DynamicDrawUsage;
52
- color.usage = DynamicDrawUsage;
40
+ // One interleaved buffer holds every per glyph attribute. WebGPU allows 8 vertex
41
+ // buffers per pipeline, and one buffer per attribute would take 9 with position
42
+ // and uv.
43
+ const glyphs = new InstancedInterleavedBuffer(new Float32Array(max_allocations * GLYPH_ATTRIBUTES.length * 4), GLYPH_ATTRIBUTES.length * 4);
44
+ glyphs.setUsage(DynamicDrawUsage);
53
45
 
54
- instanced_geometry.setAttribute('transformsCol0', transformsCol0);
55
- instanced_geometry.setAttribute('transformsCol1', transformsCol1);
56
- instanced_geometry.setAttribute('transformsCol2', transformsCol2);
57
- instanced_geometry.setAttribute('transformsCol3', transformsCol3);
58
-
59
- instanced_geometry.setAttribute('glyph_bounds', glyph_bounds);
60
- instanced_geometry.setAttribute('plane_bounds', plane_bounds);
61
- instanced_geometry.setAttribute('color', color);
46
+ GLYPH_ATTRIBUTES.forEach((name, i) =>
47
+ {
48
+ instanced_geometry.setAttribute(name, new InterleavedBufferAttribute(glyphs, 4, i * 4));
49
+ });
62
50
 
63
51
  super(instanced_geometry, new SDFTextMaterial(atlas_texture));
64
52
 
@@ -195,6 +183,8 @@ class SDFTextBatch extends Mesh
195
183
  {
196
184
  this.geometry.dispose();
197
185
  this.material.dispose();
186
+
187
+ super.dispose();
198
188
  }
199
189
  }
200
190
 
@@ -0,0 +1,399 @@
1
+ import { SDFTextBatch } from '../components/sdf_text/SDFTextBatch';
2
+ import { WorldImage } from '../components/WorldImage';
3
+
4
+ import type { Material, Mesh, Object3D, Texture } from 'three';
5
+ import { Box3, Color as ThreeColor, Sphere, TextureLoader, Vector3 } from 'three';
6
+
7
+ type Color = number | string;
8
+
9
+ interface DebugLike
10
+ {
11
+ draw_cube(pos?: Vector3, size?: number, color?: Color): Object3D;
12
+ draw_sphere(pos: Vector3, size: number, color?: Color): Object3D;
13
+ draw_plane(width?: number, height?: number, color?: Color): Object3D;
14
+ draw_math_sphere(sphere: Sphere, color?: Color): Object3D;
15
+ draw_bounding_box(bb: Box3, color?: Color): Object3D;
16
+ draw_label(text: string, pos?: Vector3, size?: number, color?: Color): Object3D;
17
+ scene: Object3D;
18
+ }
19
+
20
+ interface DebugDrawRequest
21
+ {
22
+ shape?: unknown;
23
+ position?: unknown;
24
+ size?: unknown;
25
+ color?: unknown;
26
+ object?: unknown;
27
+ text?: unknown;
28
+ font?: unknown;
29
+ }
30
+
31
+ // An msdf-atlas-gen layout (the JSON it writes with -json) and its atlas image.
32
+ interface SDFFont
33
+ {
34
+ layout: unknown;
35
+ atlas: Texture;
36
+ }
37
+
38
+ type SDFFontLoader = (layout_url: string, atlas_url: string) => Promise<SDFFont>;
39
+
40
+ interface DebugDrawerOptions
41
+ {
42
+ // URL of the font sdf_text uses when the request names none.
43
+ sdf_font?: string;
44
+ // Loads a font. Defaults to fetching the layout and loading the atlas image.
45
+ load_sdf_font?: SDFFontLoader;
46
+ }
47
+
48
+ interface SDFGlyph
49
+ {
50
+ unicode: number;
51
+ planeBounds?: unknown;
52
+ }
53
+
54
+ interface SDFLayout
55
+ {
56
+ atlas: { width: number, height: number, yOrigin?: string };
57
+ glyphs: SDFGlyph[];
58
+ }
59
+
60
+ interface DebugDrawResult
61
+ {
62
+ id: string;
63
+ shape: string;
64
+ helpers: number;
65
+ }
66
+
67
+ interface DebugClearRequest
68
+ {
69
+ id?: unknown;
70
+ }
71
+
72
+ interface DebugClearResult
73
+ {
74
+ removed: number;
75
+ helpers: number;
76
+ }
77
+
78
+ const SHAPES = ['cube', 'sphere', 'plane', 'math_sphere', 'bounding_box', 'label', 'sdf_text'];
79
+
80
+ // At 48px a glyph is about 27 pixels wide, so 200 keeps the label canvas below the
81
+ // 8192 pixel texture limit WebGPU guarantees.
82
+ const MAX_LABEL_LENGTH = 200;
83
+
84
+ async function load_sdf_font(layout_url: string, atlas_url: string): Promise<SDFFont>
85
+ {
86
+ const response = await fetch(layout_url);
87
+
88
+ if (!response.ok)
89
+ {
90
+ throw new Error(`HTTP ${response.status}`);
91
+ }
92
+
93
+ // TextureLoader rejects with the image's error event, which says nothing useful.
94
+ const atlas_request = new TextureLoader().loadAsync(atlas_url).catch(() =>
95
+ {
96
+ throw new Error(`no atlas image at ${atlas_url}`);
97
+ });
98
+ const [layout, atlas] = await Promise.all([response.json() as Promise<unknown>, atlas_request]);
99
+
100
+ return { layout, atlas };
101
+ }
102
+
103
+ // Only helpers drawn through this class are tracked, so debug_clear never touches the
104
+ // helpers the app draws for itself into Debug.scene.
105
+ class DebugDrawer
106
+ {
107
+ private helpers = new Map<string, Object3D>();
108
+ private sdf_font: string | undefined;
109
+ private load_sdf_font: SDFFontLoader;
110
+ // Loaded fonts by layout URL. Every sdf_text helper shares its font's atlas, so
111
+ // clearing a helper never disposes it.
112
+ private sdf_fonts = new Map<string, Promise<SDFFont>>();
113
+
114
+ constructor(options: DebugDrawerOptions = {})
115
+ {
116
+ this.sdf_font = options.sdf_font;
117
+ this.load_sdf_font = options.load_sdf_font ?? load_sdf_font;
118
+ }
119
+
120
+ // sdf_text loads its font first, so it resolves later. Every other shape draws now.
121
+ draw(debug: DebugLike, root: Object3D, request: DebugDrawRequest): DebugDrawResult | Promise<DebugDrawResult>
122
+ {
123
+ const shape = typeof request.shape === 'string' && SHAPES.includes(request.shape) ? request.shape : null;
124
+
125
+ if (shape === null)
126
+ {
127
+ throw this.error('bad_request', `Unknown shape. Valid shapes: ${SHAPES.join(', ')}.`);
128
+ }
129
+
130
+ const position = this.position(request.position);
131
+ const size = this.size(request.size);
132
+ const color = this.color(request.color);
133
+
134
+ if (shape === 'sdf_text')
135
+ {
136
+ return this.draw_sdf_text(debug, this.text(request.text), this.font_url(request.font), position, size, color);
137
+ }
138
+
139
+ return this.track(shape, this.build(debug, root, shape, request, position, size, color));
140
+ }
141
+
142
+ private track(shape: string, helper: Object3D): DebugDrawResult
143
+ {
144
+ this.helpers.set(helper.uuid, helper);
145
+
146
+ return { id: helper.uuid, shape, helpers: this.helpers.size };
147
+ }
148
+
149
+ private async draw_sdf_text(debug: DebugLike, text: string, font_url: string, position: Vector3, size: number, color: Color | undefined): Promise<DebugDrawResult>
150
+ {
151
+ const font = await this.font(font_url);
152
+ const layout = this.sdf_layout(font.layout, font_url);
153
+
154
+ // SDFText looks glyphs up by UTF-16 unit (charCodeAt), so this check does too.
155
+ const drawable = Array.from({ length: text.length }, (_, i) => text.charCodeAt(i));
156
+
157
+ if (!drawable.some((unit) => layout.glyphs.some((glyph) => glyph.unicode === unit && glyph.planeBounds !== undefined)))
158
+ {
159
+ throw this.error('bad_request', `The font at ${font_url} has none of the characters of the text.`);
160
+ }
161
+
162
+ // One batch per helper, so debug_clear can remove each text on its own.
163
+ const batch = new SDFTextBatch(layout, font.atlas);
164
+ const element = batch.add_text(text);
165
+ element.set_position(position);
166
+ element.set_size(size);
167
+ element.set_color(new ThreeColor(color ?? 0xffffff));
168
+ // The text never changes, so one upload is enough.
169
+ batch.update(true);
170
+ debug.scene.add(batch);
171
+
172
+ return this.track('sdf_text', batch);
173
+ }
174
+
175
+ private font(url: string): Promise<SDFFont>
176
+ {
177
+ let font = this.sdf_fonts.get(url);
178
+
179
+ if (font === undefined)
180
+ {
181
+ font = this.load_sdf_font(url, url.replace(/\.json(?=$|[?#])/, '.png')).catch((cause: unknown) =>
182
+ {
183
+ // Forget the failure, so the next request tries again.
184
+ this.sdf_fonts.delete(url);
185
+
186
+ const reason = cause instanceof Error ? cause.message : typeof cause === 'string' ? cause : 'unknown error';
187
+
188
+ throw this.error('not_found', `Could not load the SDF font at ${url}: ${reason}`);
189
+ });
190
+
191
+ this.sdf_fonts.set(url, font);
192
+ }
193
+
194
+ return font;
195
+ }
196
+
197
+ private font_url(value: unknown): string
198
+ {
199
+ if (value === undefined && this.sdf_font === undefined)
200
+ {
201
+ throw this.error('bad_request', 'sdf_text needs a font: no default SDF font is configured.');
202
+ }
203
+
204
+ const url = value ?? this.sdf_font;
205
+
206
+ if (typeof url !== 'string' || !/\.json(?=$|[?#])/.test(url))
207
+ {
208
+ throw this.error('bad_request', 'font must be the URL of an msdf-atlas-gen .json layout, with its .png atlas next to it.');
209
+ }
210
+
211
+ return url;
212
+ }
213
+
214
+ private sdf_layout(value: unknown, url: string): SDFLayout
215
+ {
216
+ const layout = value as Partial<SDFLayout> | null;
217
+
218
+ if (typeof layout?.atlas?.width !== 'number' || typeof layout.atlas.height !== 'number' || !Array.isArray(layout.glyphs))
219
+ {
220
+ throw this.error('bad_request', `The font at ${url} is not an msdf-atlas-gen layout.`);
221
+ }
222
+
223
+ // The material maps atlas bounds counted from the bottom.
224
+ if (layout.atlas.yOrigin === 'top')
225
+ {
226
+ throw this.error('bad_request', `The font at ${url} has yOrigin top. Generate it with -yorigin bottom.`);
227
+ }
228
+
229
+ return layout as SDFLayout;
230
+ }
231
+
232
+ private build(debug: DebugLike, root: Object3D, shape: string, request: DebugDrawRequest, position: Vector3, size: number, color: Color | undefined): Object3D
233
+ {
234
+ switch (shape)
235
+ {
236
+ case 'cube':
237
+ return debug.draw_cube(position, size, color);
238
+
239
+ case 'sphere':
240
+ // Sphere's size is its radius, which is what the contract promises.
241
+ return debug.draw_sphere(position, size, color);
242
+
243
+ case 'plane':
244
+ {
245
+ const plane = debug.draw_plane(size, size, color);
246
+ plane.position.copy(position);
247
+
248
+ return plane;
249
+ }
250
+
251
+ case 'math_sphere':
252
+ return debug.draw_math_sphere(new Sphere(position, size), color);
253
+
254
+ case 'label':
255
+ // Size is the text height.
256
+ return debug.draw_label(this.text(request.text), position, size, color);
257
+
258
+ default:
259
+ return debug.draw_bounding_box(this.bounds(root, request.object), color);
260
+ }
261
+ }
262
+
263
+ clear(request: DebugClearRequest): DebugClearResult
264
+ {
265
+ if (request.id === undefined)
266
+ {
267
+ const removed = this.helpers.size;
268
+
269
+ this.helpers.forEach((helper) => this.dispose(helper));
270
+ this.helpers.clear();
271
+
272
+ return { removed, helpers: 0 };
273
+ }
274
+
275
+ const helper = typeof request.id === 'string' ? this.helpers.get(request.id) : undefined;
276
+
277
+ if (helper === undefined)
278
+ {
279
+ throw this.error('not_found', 'No helper with that id was drawn through debug_draw.');
280
+ }
281
+
282
+ this.dispose(helper);
283
+ this.helpers.delete(helper.uuid);
284
+
285
+ return { removed: 1, helpers: this.helpers.size };
286
+ }
287
+
288
+ private dispose(helper: Object3D): void
289
+ {
290
+ helper.removeFromParent();
291
+
292
+ helper.traverse((node) =>
293
+ {
294
+ const mesh = node as Partial<Mesh>;
295
+
296
+ mesh.geometry?.dispose();
297
+
298
+ const materials: Material[] = Array.isArray(mesh.material) ? mesh.material : mesh.material ? [mesh.material] : [];
299
+ materials.forEach((material) => material.dispose());
300
+
301
+ // A label owns its canvas texture.
302
+ if (node instanceof WorldImage)
303
+ {
304
+ node.material.uniforms._MainTex.value.dispose();
305
+ }
306
+ });
307
+ }
308
+
309
+ private bounds(root: Object3D, target: unknown): Box3
310
+ {
311
+ const { name, uuid } = (typeof target === 'object' && target !== null ? target : {}) as { name?: unknown, uuid?: unknown };
312
+
313
+ if (typeof uuid !== 'string' && typeof name !== 'string')
314
+ {
315
+ throw this.error('bad_request', 'bounding_box needs an object with a name or a uuid.');
316
+ }
317
+
318
+ // uuid wins over name, like SceneEditor.
319
+ const found = typeof uuid === 'string'
320
+ ? root.getObjectByProperty('uuid', uuid)
321
+ : root.getObjectByName(name as string);
322
+
323
+ if (found === undefined)
324
+ {
325
+ const label = typeof uuid === 'string' ? `uuid ${uuid}` : `name ${name as string}`;
326
+
327
+ throw this.error('not_found', `No object matching ${label} in the current scene.`);
328
+ }
329
+
330
+ const box = new Box3().setFromObject(found);
331
+
332
+ if (box.isEmpty())
333
+ {
334
+ throw this.error('bad_request', `Object "${found.name || found.uuid}" has no geometry, so its bounds are empty.`);
335
+ }
336
+
337
+ return box;
338
+ }
339
+
340
+ private text(value: unknown): string
341
+ {
342
+ if (typeof value !== 'string' || value.length === 0 || value.length > MAX_LABEL_LENGTH)
343
+ {
344
+ throw this.error('bad_request', `label and sdf_text need a text of 1 to ${MAX_LABEL_LENGTH} characters.`);
345
+ }
346
+
347
+ return value;
348
+ }
349
+
350
+ private position(value: unknown): Vector3
351
+ {
352
+ if (value === undefined)
353
+ {
354
+ return new Vector3();
355
+ }
356
+
357
+ if (!Array.isArray(value) || value.length !== 3 || !value.every((n) => typeof n === 'number' && Number.isFinite(n)))
358
+ {
359
+ throw this.error('bad_request', 'position must be [x, y, z] with finite numbers.');
360
+ }
361
+
362
+ return new Vector3(value[0] as number, value[1] as number, value[2] as number);
363
+ }
364
+
365
+ private size(value: unknown): number
366
+ {
367
+ if (value === undefined)
368
+ {
369
+ return 1;
370
+ }
371
+
372
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
373
+ {
374
+ throw this.error('bad_request', 'size must be a positive finite number.');
375
+ }
376
+
377
+ return value;
378
+ }
379
+
380
+ private color(value: unknown): Color | undefined
381
+ {
382
+ if (value === undefined || typeof value === 'number' || typeof value === 'string')
383
+ {
384
+ return value as Color | undefined;
385
+ }
386
+
387
+ throw this.error('bad_request', 'color must be a number or a string.');
388
+ }
389
+
390
+ private error(code: string, message: string): Error
391
+ {
392
+ const error: Error & { code?: string } = new Error(message);
393
+ error.code = code;
394
+
395
+ return error;
396
+ }
397
+ }
398
+
399
+ export { DebugDrawer };