babylon.quarks-editor 0.26.0 → 0.28.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 (211) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +69 -9
  3. package/dist/core/assetDragSession.d.ts +1 -1
  4. package/dist/core/assetTree.d.ts +6 -1
  5. package/dist/core/assets.d.ts +7 -0
  6. package/dist/core/assets.js +11 -7
  7. package/dist/core/binding.d.ts +11 -2
  8. package/dist/core/binding.js +9 -1
  9. package/dist/core/collision.d.ts +4 -0
  10. package/dist/core/collision.js +4 -0
  11. package/dist/core/colors.d.ts +16 -0
  12. package/dist/core/colors.js +8 -0
  13. package/dist/core/defaultEffect.d.ts +5 -4
  14. package/dist/core/defaultEffect.js +4 -4
  15. package/dist/core/editorScene.d.ts +1 -1
  16. package/dist/core/editorSession.d.ts +229 -0
  17. package/dist/core/editorSession.js +983 -0
  18. package/dist/core/effectTree.d.ts +6 -1
  19. package/dist/core/effectTree.js +13 -88
  20. package/dist/core/emitterShapeWireframe.d.ts +2 -2
  21. package/dist/core/emitterShapeWireframe.js +45 -13
  22. package/dist/core/galleryDropMarker.d.ts +2 -2
  23. package/dist/core/galleryDropMarker.js +6 -6
  24. package/dist/core/galleryLayout.d.ts +3 -3
  25. package/dist/core/galleryLayout.js +1 -1
  26. package/dist/core/galleryPlayback.d.ts +1 -1
  27. package/dist/core/galleryPlayback.js +2 -1
  28. package/dist/core/galleryScene.d.ts +4 -4
  29. package/dist/core/galleryScene.js +3 -3
  30. package/dist/core/gallerySelectionMarker.d.ts +4 -4
  31. package/dist/core/gallerySelectionMarker.js +5 -5
  32. package/dist/core/groups.d.ts +1 -1
  33. package/dist/core/groups.js +1 -1
  34. package/dist/core/history.d.ts +7 -1
  35. package/dist/core/history.js +4 -0
  36. package/dist/core/indexedDbProjectStore.d.ts +14 -1
  37. package/dist/core/indexedDbProjectStore.js +13 -0
  38. package/dist/core/lineShaders.d.ts +2 -2
  39. package/dist/core/lineShaders.js +2 -2
  40. package/dist/core/loadEffect.d.ts +2 -2
  41. package/dist/core/loadEffect.js +1 -1
  42. package/dist/core/material.d.ts +12 -2
  43. package/dist/core/pickAssetFiles.js +1 -1
  44. package/dist/core/playback.d.ts +6 -0
  45. package/dist/core/projectStore.d.ts +26 -0
  46. package/dist/core/projectStore.js +1 -0
  47. package/dist/core/sceneEntries.d.ts +7 -4
  48. package/dist/core/sceneEntries.js +2 -2
  49. package/dist/core/scrub.d.ts +2 -2
  50. package/dist/core/scrub.js +2 -2
  51. package/dist/core/shapes.d.ts +25 -1
  52. package/dist/core/shapes.js +5 -0
  53. package/dist/core/systems.d.ts +4 -2
  54. package/dist/core/systems.js +3 -1
  55. package/dist/core/textureExport.d.ts +1 -1
  56. package/dist/core/timeline.d.ts +25 -5
  57. package/dist/core/transport.d.ts +3 -3
  58. package/dist/core/transport.js +2 -2
  59. package/dist/core/values.d.ts +21 -4
  60. package/dist/core/values.js +5 -0
  61. package/dist/core/viewportPicker.d.ts +4 -4
  62. package/dist/core/viewportPicker.js +8 -7
  63. package/dist/index.d.ts +25 -21
  64. package/dist/index.js +12 -12
  65. package/dist/react/AssetsPanel.d.ts +9 -2
  66. package/dist/react/AssetsPanel.js +6 -6
  67. package/dist/react/ChoiceDialog.d.ts +7 -0
  68. package/dist/react/ChoiceDialog.js +1 -1
  69. package/dist/react/ContextMenu.d.ts +10 -1
  70. package/dist/react/ContextMenu.js +1 -1
  71. package/dist/react/CurveEditor.d.ts +1 -1
  72. package/dist/react/CurveEditor.js +2 -2
  73. package/dist/react/EditorWorkspace.d.ts +9 -2
  74. package/dist/react/EditorWorkspace.js +10 -7
  75. package/dist/react/EffectEditor.d.ts +11 -5
  76. package/dist/react/EffectEditor.js +7 -7
  77. package/dist/react/EffectEditorHost.d.ts +33 -29
  78. package/dist/react/EffectEditorHost.js +247 -1674
  79. package/dist/react/EffectTreeView.d.ts +4 -1
  80. package/dist/react/EffectTreeView.js +3 -3
  81. package/dist/react/GradientEditor.d.ts +1 -1
  82. package/dist/react/GradientEditor.js +3 -3
  83. package/dist/react/InspectorInfoIcon.js +3 -3
  84. package/dist/react/InspectorLabel.js +3 -3
  85. package/dist/react/MessageDialog.d.ts +5 -0
  86. package/dist/react/MessageDialog.js +1 -1
  87. package/dist/react/ModuleSection.d.ts +10 -0
  88. package/dist/react/ModuleSection.js +8 -5
  89. package/dist/react/ObjectModule.d.ts +1 -1
  90. package/dist/react/ObjectModule.js +4 -4
  91. package/dist/react/ProjectsPage.d.ts +7 -1
  92. package/dist/react/ProjectsPage.js +33 -17
  93. package/dist/react/PromptDialog.d.ts +7 -0
  94. package/dist/react/PromptDialog.js +1 -1
  95. package/dist/react/ScenePanel.d.ts +9 -2
  96. package/dist/react/ScenePanel.js +12 -10
  97. package/dist/react/SubEmittersModule.d.ts +1 -1
  98. package/dist/react/SubEmittersModule.js +5 -5
  99. package/dist/react/TextureSelect.d.ts +24 -0
  100. package/dist/react/TextureSelect.js +74 -0
  101. package/dist/react/TextureUVField.d.ts +14 -0
  102. package/dist/react/TextureUVField.js +23 -0
  103. package/dist/react/TimelinePanel.d.ts +20 -7
  104. package/dist/react/TimelinePanel.js +21 -15
  105. package/dist/react/TimelineTrackRow.d.ts +13 -3
  106. package/dist/react/TimelineTrackRow.js +6 -6
  107. package/dist/react/TransportBar.d.ts +13 -4
  108. package/dist/react/TransportBar.js +3 -3
  109. package/dist/react/ValueField.d.ts +32 -5
  110. package/dist/react/ValueField.js +22 -11
  111. package/dist/react/behaviorModules.d.ts +19 -6
  112. package/dist/react/behaviorModules.js +68 -17
  113. package/dist/react/fields.d.ts +9 -0
  114. package/dist/react/fields.js +6 -3
  115. package/dist/react/host/BottomDocks.d.ts +36 -0
  116. package/dist/react/host/BottomDocks.js +63 -0
  117. package/dist/react/host/HostDialogs.d.ts +50 -0
  118. package/dist/react/host/HostDialogs.js +113 -0
  119. package/dist/react/host/InspectorSidebar.d.ts +32 -0
  120. package/dist/react/host/InspectorSidebar.js +59 -0
  121. package/dist/react/host/SceneSidebar.d.ts +53 -0
  122. package/dist/react/host/SceneSidebar.js +62 -0
  123. package/dist/react/host/ViewportPane.d.ts +56 -0
  124. package/dist/react/host/ViewportPane.js +91 -0
  125. package/dist/react/host/hostHooks.d.ts +44 -0
  126. package/dist/react/host/hostHooks.js +164 -0
  127. package/dist/react/host/hostStyles.d.ts +21 -0
  128. package/dist/react/host/hostStyles.js +57 -0
  129. package/dist/react/index.d.ts +42 -38
  130. package/dist/react/index.js +25 -23
  131. package/dist/react/inspectorHints.js +31 -0
  132. package/dist/react/modules.d.ts +19 -6
  133. package/dist/react/modules.js +134 -117
  134. package/dist/react/theme.d.ts +15 -0
  135. package/dist/react/theme.js +15 -0
  136. package/dist/session.d.ts +9 -0
  137. package/dist/session.js +8 -0
  138. package/package.json +18 -11
  139. package/src/core/assetDragSession.ts +1 -1
  140. package/src/core/assetTree.ts +15 -2
  141. package/src/core/assets.ts +25 -10
  142. package/src/core/binding.ts +12 -3
  143. package/src/core/collision.ts +4 -0
  144. package/src/core/colors.ts +21 -1
  145. package/src/core/defaultEffect.ts +8 -7
  146. package/src/core/editorScene.ts +1 -1
  147. package/src/core/editorSession.ts +1299 -0
  148. package/src/core/effectTree.ts +18 -101
  149. package/src/core/emitterShapeWireframe.ts +48 -16
  150. package/src/core/galleryDropMarker.ts +8 -8
  151. package/src/core/galleryLayout.ts +4 -4
  152. package/src/core/galleryPlayback.ts +3 -2
  153. package/src/core/galleryScene.ts +5 -5
  154. package/src/core/gallerySelectionMarker.ts +7 -7
  155. package/src/core/groups.ts +1 -1
  156. package/src/core/history.ts +7 -1
  157. package/src/core/indexedDbProjectStore.ts +14 -1
  158. package/src/core/lineShaders.ts +2 -2
  159. package/src/core/loadEffect.ts +4 -4
  160. package/src/core/material.ts +14 -4
  161. package/src/core/pickAssetFiles.ts +2 -2
  162. package/src/core/playback.ts +6 -0
  163. package/src/core/projectStore.ts +26 -0
  164. package/src/core/sceneEntries.ts +8 -5
  165. package/src/core/scrub.ts +3 -3
  166. package/src/core/shapes.ts +29 -2
  167. package/src/core/systems.ts +11 -4
  168. package/src/core/textureExport.ts +1 -1
  169. package/src/core/timeline.ts +43 -18
  170. package/src/core/transport.ts +4 -4
  171. package/src/core/values.ts +20 -4
  172. package/src/core/viewportPicker.ts +12 -11
  173. package/src/index.ts +25 -21
  174. package/src/react/AssetsPanel.tsx +13 -6
  175. package/src/react/ChoiceDialog.tsx +8 -1
  176. package/src/react/ContextMenu.tsx +11 -2
  177. package/src/react/CurveEditor.tsx +2 -2
  178. package/src/react/EditorWorkspace.tsx +16 -10
  179. package/src/react/EffectEditor.tsx +17 -9
  180. package/src/react/EffectEditorHost.tsx +415 -2354
  181. package/src/react/EffectTreeView.tsx +7 -4
  182. package/src/react/GradientEditor.tsx +3 -3
  183. package/src/react/InspectorInfoIcon.tsx +3 -3
  184. package/src/react/InspectorLabel.tsx +3 -3
  185. package/src/react/MessageDialog.tsx +6 -1
  186. package/src/react/ModuleSection.tsx +15 -4
  187. package/src/react/ObjectModule.tsx +5 -5
  188. package/src/react/ProjectsPage.tsx +44 -17
  189. package/src/react/PromptDialog.tsx +8 -1
  190. package/src/react/ScenePanel.tsx +21 -12
  191. package/src/react/SubEmittersModule.tsx +6 -6
  192. package/src/react/TextureSelect.tsx +118 -0
  193. package/src/react/TextureUVField.tsx +62 -0
  194. package/src/react/TimelinePanel.tsx +40 -21
  195. package/src/react/TimelineTrackRow.tsx +19 -9
  196. package/src/react/TransportBar.tsx +16 -7
  197. package/src/react/ValueField.tsx +60 -16
  198. package/src/react/behaviorModules.tsx +240 -55
  199. package/src/react/fields.tsx +22 -4
  200. package/src/react/host/BottomDocks.tsx +178 -0
  201. package/src/react/host/HostDialogs.tsx +228 -0
  202. package/src/react/host/InspectorSidebar.tsx +147 -0
  203. package/src/react/host/SceneSidebar.tsx +227 -0
  204. package/src/react/host/ViewportPane.tsx +300 -0
  205. package/src/react/host/hostHooks.ts +198 -0
  206. package/src/react/host/hostStyles.ts +65 -0
  207. package/src/react/index.ts +43 -38
  208. package/src/react/inspectorHints.ts +40 -0
  209. package/src/react/modules.tsx +440 -170
  210. package/src/react/theme.ts +15 -0
  211. package/src/session.ts +17 -0
@@ -1,7 +1,20 @@
1
- import {Texture} from '@babylonjs/core/Materials/Textures/texture';
2
- import {Mesh} from '@babylonjs/core/Meshes/mesh';
1
+ import {Texture} from '@babylonjs/core/Materials/Textures/texture.js';
2
+ import {Mesh} from '@babylonjs/core/Meshes/mesh.js';
3
3
  import {PlusIcon, XMarkIcon} from '@heroicons/react/24/solid';
4
- import type {Behavior, CullingMode, MeshLighting, MeshRim, ParticleSystem, SortMode} from 'babylon.quarks';
4
+ import type {
5
+ Behavior,
6
+ CullingMode,
7
+ GradientMapChannel,
8
+ MeshLighting,
9
+ MeshRim,
10
+ ParticleErosionOptions,
11
+ ParticleSystem,
12
+ ParticleUVDistortionOptions,
13
+ RenderFace,
14
+ ScalingMode,
15
+ SortMode,
16
+ TrailSettings,
17
+ } from 'babylon.quarks';
5
18
  import {
6
19
  AxisAngleGenerator,
7
20
  BoxEmitFrom,
@@ -17,12 +30,13 @@ import {
17
30
  RandomQuatGenerator,
18
31
  RenderMode,
19
32
  SizeOverLife,
33
+ Vector2,
20
34
  Vector3,
21
35
  Vector3Function,
22
36
  Vector4,
37
+ normalizeTextureUV,
23
38
  } from 'babylon.quarks';
24
- import React, {useMemo, useState} from 'react';
25
- import {EffectBinding} from '../core/binding';
39
+ import React, {useMemo} from 'react';
26
40
  import {
27
41
  DEFAULT_GRADIENT_STOPS,
28
42
  GradientStop,
@@ -31,9 +45,9 @@ import {
31
45
  hexToRgb,
32
46
  readGradientStops,
33
47
  rgbToHex,
34
- } from '../core/colors';
35
- import {applyRendererMaterial, getMaterialLabel} from '../core/material';
36
- import type {ShapeType} from '../core/shapes';
48
+ } from '../core/colors.js';
49
+ import {applyRendererMaterial, getMaterialLabel} from '../core/material.js';
50
+ import type {ShapeType} from '../core/shapes.js';
37
51
  import {
38
52
  DEFAULT_SHAPE_PARAMS,
39
53
  SHAPE_PARAM_KEYS,
@@ -41,21 +55,18 @@ import {
41
55
  createShape,
42
56
  getShapeType,
43
57
  readShapeParams,
44
- } from '../core/shapes';
45
- import {buildCurve, buildScalar, readPieces, readScalar} from '../core/values';
46
- import {CurveEditor} from './CurveEditor';
47
- import {GradientEditor} from './GradientEditor';
48
- import {InspectorInfoIcon} from './InspectorInfoIcon';
49
- import {ModuleSection} from './ModuleSection';
50
- import {PromptDialog} from './PromptDialog';
51
- import {ValueField} from './ValueField';
52
- import {CheckboxField, NumberField, Row, SelectField} from './fields';
53
- import {iconStyle} from './icons';
54
- import {theme} from './theme';
55
-
56
- interface ModuleProps {
57
- binding: EffectBinding;
58
- }
58
+ } from '../core/shapes.js';
59
+ import {buildCurve, buildScalar, readPieces, readScalar} from '../core/values.js';
60
+ import {CurveEditor} from './CurveEditor.js';
61
+ import {GradientEditor} from './GradientEditor.js';
62
+ import {InspectorInfoIcon} from './InspectorInfoIcon.js';
63
+ import {ModuleSection, type ModuleProps} from './ModuleSection.js';
64
+ import {TextureSelect} from './TextureSelect.js';
65
+ import {TextureUVField} from './TextureUVField.js';
66
+ import {ValueField} from './ValueField.js';
67
+ import {CheckboxField, NumberField, Row, SelectField} from './fields.js';
68
+ import {iconStyle} from './icons.js';
69
+ import {theme} from './theme.js';
59
70
 
60
71
  const MESH_QUAD_POSITIONS = new Float32Array([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0]);
61
72
  const MESH_QUAD_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
@@ -105,6 +116,49 @@ const PARAM_LABELS: {[key: string]: string} = {
105
116
  speed: 'Speed (lengths/s)',
106
117
  };
107
118
 
119
+ /** Uppercase group title inside a module, as the Renderer's "Material" heading. */
120
+ function SubHeader(props: {title: string; hint: string}) {
121
+ return (
122
+ <div
123
+ style={{
124
+ marginTop: 10,
125
+ marginBottom: 2,
126
+ fontSize: 11,
127
+ fontWeight: 600,
128
+ color: '#7c8db5',
129
+ textTransform: 'uppercase',
130
+ letterSpacing: 0.4,
131
+ display: 'flex',
132
+ alignItems: 'center',
133
+ gap: 5,
134
+ }}
135
+ >
136
+ {props.title}
137
+ <InspectorInfoIcon content={props.hint} size={12} />
138
+ </div>
139
+ );
140
+ }
141
+
142
+ /** Three number inputs editing a Vector3 by value. */
143
+ function AxisField(props: {value: Vector3; onChange: (next: Vector3) => void}) {
144
+ return (
145
+ <div style={{display: 'flex', gap: 6}}>
146
+ {(['x', 'y', 'z'] as const).map((axis) => (
147
+ <NumberField
148
+ key={axis}
149
+ value={props.value[axis]}
150
+ step={0.1}
151
+ onChange={(v) => {
152
+ const next = props.value.clone();
153
+ next[axis] = v;
154
+ props.onChange(next);
155
+ }}
156
+ />
157
+ ))}
158
+ </div>
159
+ );
160
+ }
161
+
108
162
  function ColorInput(props: {value: Vector4; onChange: (next: Vector4) => void}) {
109
163
  const {value, onChange} = props;
110
164
  return (
@@ -136,18 +190,20 @@ function ColorInput(props: {value: Vector4; onChange: (next: Vector4) => void})
136
190
  );
137
191
  }
138
192
 
193
+ /** The Main module (Unity's main module): duration, looping, start delay, life, speed, size, rotation and colour. */
139
194
  export function MainModule({binding}: ModuleProps) {
140
195
  const system = binding.system;
141
196
  const startColor = system.startColor;
142
197
  const colorMode =
143
198
  startColor instanceof RandomColor ? 'random' : startColor instanceof ColorRange ? 'range' : 'constant';
144
- const twoColors = startColor instanceof RandomColor || startColor instanceof ColorRange;
145
- const colorA = twoColors
146
- ? (startColor as never as {a: Vector4}).a
199
+ const twoColor = startColor instanceof RandomColor || startColor instanceof ColorRange ? startColor : null;
200
+ const colorA = twoColor
201
+ ? twoColor.a
147
202
  : startColor instanceof ConstantColor
148
203
  ? startColor.color
149
204
  : new Vector4(1, 1, 1, 1);
150
- const colorB = twoColors ? (startColor as never as {b: Vector4}).b : new Vector4(1, 0.5, 0.2, 1);
205
+ const colorB = twoColor ? twoColor.b : new Vector4(1, 0.5, 0.2, 1);
206
+ const twoColors = twoColor !== null;
151
207
  /** The two-colour generator of the current mode. */
152
208
  const makeTwoColors = (a: Vector4, b: Vector4) =>
153
209
  colorMode === 'range' ? new ColorRange(a, b) : new RandomColor(a, b);
@@ -161,17 +217,17 @@ export function MainModule({binding}: ModuleProps) {
161
217
  ? 'euler'
162
218
  : isAxisAngle
163
219
  ? 'axisAngle'
164
- : readScalar(rotation as never).mode === 'random'
220
+ : readScalar(rotation).mode === 'random'
165
221
  ? 'random'
166
222
  : 'angle';
167
223
  const rotationState = readScalar(
168
- rotation instanceof RandomQuatGenerator || isEuler || isAxisAngle ? undefined : (rotation as never)
224
+ rotation instanceof RandomQuatGenerator || isEuler || isAxisAngle ? undefined : rotation
169
225
  );
170
226
  const axisAngle = isAxisAngle ? (rotation as AxisAngleGenerator) : undefined;
171
227
  const eulerAngles = {
172
- x: isEuler ? readScalar((rotation as EulerGenerator).angleX as never).value : 0,
173
- y: isEuler ? readScalar((rotation as EulerGenerator).angleY as never).value : 0,
174
- z: isEuler ? readScalar((rotation as EulerGenerator).angleZ as never).value : 0,
228
+ x: isEuler ? readScalar((rotation as EulerGenerator).angleX).value : 0,
229
+ y: isEuler ? readScalar((rotation as EulerGenerator).angleY).value : 0,
230
+ z: isEuler ? readScalar((rotation as EulerGenerator).angleZ).value : 0,
175
231
  };
176
232
  const setEulerAxis = (axis: 'x' | 'y' | 'z', v: number) =>
177
233
  binding.apply((s) => {
@@ -182,8 +238,7 @@ export function MainModule({binding}: ModuleProps) {
182
238
  axis === 'z' ? new ConstantValue(v) : cur.angleZ
183
239
  );
184
240
  });
185
- const startSize = system.startSize as unknown as {type?: string; x?: never; y?: never; z?: never};
186
- const size3D = startSize.type === 'vec3function';
241
+ const startSize3D = system.startSize instanceof Vector3Function ? system.startSize : null;
187
242
  const colorGenMode =
188
243
  startColor instanceof RandomColorBetweenGradient
189
244
  ? 'gradient2'
@@ -205,9 +260,31 @@ export function MainModule({binding}: ModuleProps) {
205
260
  <Row label="World space">
206
261
  <CheckboxField value={system.worldSpace} onChange={(v) => binding.apply((s) => (s.worldSpace = v))} />
207
262
  </Row>
263
+ {system.worldSpace && (
264
+ <Row label="Scaling mode">
265
+ <SelectField<ScalingMode>
266
+ value={system.scalingMode}
267
+ options={[
268
+ {value: 'shape', label: 'Shape (sizes and shape only)'},
269
+ {value: 'hierarchy', label: 'Hierarchy (motion scales too)'},
270
+ ]}
271
+ onChange={(mode) => binding.apply((s) => (s.scalingMode = mode))}
272
+ />
273
+ </Row>
274
+ )}
208
275
  <Row label="Prewarm">
209
276
  <CheckboxField value={system.prewarm} onChange={(v) => binding.apply((s) => (s.prewarm = v))} />
210
277
  </Row>
278
+ {system.prewarm && (
279
+ <Row label="Warm-up time">
280
+ <NumberField
281
+ value={system.prewarmTime}
282
+ step={0.1}
283
+ min={0}
284
+ onChange={(v) => binding.apply((s) => (s.prewarmTime = Math.max(0, v)))}
285
+ />
286
+ </Row>
287
+ )}
211
288
  <Row label="Playback speed">
212
289
  <NumberField
213
290
  value={system.playbackSpeed}
@@ -269,50 +346,50 @@ export function MainModule({binding}: ModuleProps) {
269
346
  />
270
347
  <Row label="Start size">
271
348
  <SelectField
272
- value={size3D ? '3d' : 'uniform'}
349
+ value={startSize3D ? '3d' : 'uniform'}
273
350
  options={[
274
351
  {value: 'uniform', label: 'Uniform'},
275
352
  {value: '3d', label: '3D (per axis)'},
276
353
  ]}
277
354
  onChange={(mode) =>
278
355
  binding.apply((s) => {
279
- const cur = s.startSize as never as {clone: () => never; x?: never};
280
- if (mode === '3d') {
356
+ const cur = s.startSize;
357
+ if (mode === '3d' && cur.type !== 'vec3function') {
281
358
  // Seed all three axes from the current uniform generator.
282
359
  s.startSize = new Vector3Function(cur.clone(), cur.clone(), cur.clone());
283
- } else {
360
+ } else if (mode !== '3d' && cur instanceof Vector3Function) {
284
361
  // Collapse back to the X-axis generator.
285
- s.startSize = (s.startSize as never as {x: never}).x;
362
+ s.startSize = cur.x;
286
363
  }
287
364
  })
288
365
  }
289
366
  />
290
367
  </Row>
291
- {!size3D && (
368
+ {!startSize3D && (
292
369
  <ValueField
293
370
  label=""
294
- generator={system.startSize as never}
371
+ generator={system.startSize}
295
372
  min={0}
296
373
  curveMax={5}
297
374
  onChange={(g) => binding.apply((s) => (s.startSize = g))}
298
375
  />
299
376
  )}
300
- {size3D && (
377
+ {startSize3D && (
301
378
  <>
302
379
  {(['x', 'y', 'z'] as const).map((axis) => (
303
380
  <ValueField
304
381
  key={axis}
305
382
  label={axis.toUpperCase()}
306
- generator={startSize[axis] as never}
383
+ generator={startSize3D[axis]}
307
384
  min={0}
308
385
  curveMax={5}
309
386
  onChange={(g) =>
310
387
  binding.apply((s) => {
311
388
  const cur = s.startSize as Vector3Function;
312
389
  s.startSize = new Vector3Function(
313
- axis === 'x' ? (g as never) : cur.x,
314
- axis === 'y' ? (g as never) : cur.y,
315
- axis === 'z' ? (g as never) : cur.z
390
+ axis === 'x' ? g : cur.x,
391
+ axis === 'y' ? g : cur.y,
392
+ axis === 'z' ? g : cur.z
316
393
  );
317
394
  })
318
395
  }
@@ -505,6 +582,7 @@ export function MainModule({binding}: ModuleProps) {
505
582
  );
506
583
  }
507
584
 
585
+ /** The Emission module (Unity's Emission): rate over time and distance, and bursts. */
508
586
  export function EmissionModule({binding}: ModuleProps) {
509
587
  const system = binding.system;
510
588
  const bursts = system.emissionBursts ?? [];
@@ -554,7 +632,7 @@ export function EmissionModule({binding}: ModuleProps) {
554
632
  </Row>
555
633
  <Row label="Count">
556
634
  <NumberField
557
- value={readScalar(burst.count as never).value}
635
+ value={readScalar(burst.count).value}
558
636
  min={0}
559
637
  step={1}
560
638
  onChange={(count) =>
@@ -627,6 +705,7 @@ export function EmissionModule({binding}: ModuleProps) {
627
705
  // first texture" bug: a control that looks live but does nothing.
628
706
  const SHAPES_WITHOUT_MODE_SPREAD = new Set<ShapeType>(['point', 'grid', 'box', 'mesh_surface']);
629
707
 
708
+ /** The Shape module (Unity's Shape): the emitter shape and its settings. */
630
709
  export function ShapeModule({binding}: ModuleProps) {
631
710
  const system = binding.system;
632
711
  const type = getShapeType(system.emitterShape);
@@ -726,12 +805,11 @@ export function ShapeModule({binding}: ModuleProps) {
726
805
  );
727
806
  }
728
807
 
808
+ /** The Size over Lifetime module (Unity's Size over Lifetime): size by a curve over the particle's life. */
729
809
  export function SizeOverLifeModule({binding}: ModuleProps) {
730
810
  const system = binding.system;
731
811
  const behavior = findBehavior<SizeOverLife>(system.behaviors, 'SizeOverLife');
732
- const curve = behavior
733
- ? readScalar(behavior.size as never).curve
734
- : ([0.2, 1, 1, 0.4] as [number, number, number, number]);
812
+ const curve = behavior ? readScalar(behavior.size).curve : ([0.2, 1, 1, 0.4] as [number, number, number, number]);
735
813
  return (
736
814
  <ModuleSection
737
815
  title="Size over Lifetime"
@@ -740,21 +818,19 @@ export function SizeOverLifeModule({binding}: ModuleProps) {
740
818
  binding.apply((s) => {
741
819
  removeBehavior(s, 'SizeOverLife');
742
820
  if (enabled) {
743
- s.addBehavior(
744
- new SizeOverLife(buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve}) as never)
745
- );
821
+ s.addBehavior(new SizeOverLife(buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve})));
746
822
  }
747
823
  })
748
824
  }
749
825
  >
750
826
  <div style={{marginTop: 6}}>
751
827
  <CurveEditor
752
- pieces={behavior ? readPieces(behavior.size as never) : [{start: 0, p: curve}]}
828
+ pieces={behavior ? readPieces(behavior.size) : [{start: 0, p: curve}]}
753
829
  maxValue={2}
754
830
  onChange={(pieces) =>
755
831
  binding.apply((s) => {
756
832
  removeBehavior(s, 'SizeOverLife');
757
- s.addBehavior(new SizeOverLife(buildCurve(pieces) as never));
833
+ s.addBehavior(new SizeOverLife(buildCurve(pieces)));
758
834
  })
759
835
  }
760
836
  />
@@ -763,6 +839,7 @@ export function SizeOverLifeModule({binding}: ModuleProps) {
763
839
  );
764
840
  }
765
841
 
842
+ /** The Color over Lifetime module (Unity's Color over Lifetime): a gradient over the particle's life. */
766
843
  export function ColorOverLifeModule({binding}: ModuleProps) {
767
844
  const system = binding.system;
768
845
  const behavior = findBehavior<ColorOverLife>(system.behaviors, 'ColorOverLife');
@@ -793,8 +870,11 @@ export function ColorOverLifeModule({binding}: ModuleProps) {
793
870
  );
794
871
  }
795
872
 
873
+ /** A texture preset the host offers in the Renderer module's texture menus. */
796
874
  export interface TextureOption {
875
+ /** Its label in the menu. */
797
876
  label: string;
877
+ /** The URL the host's texture loader receives. */
798
878
  url: string;
799
879
  }
800
880
 
@@ -804,10 +884,17 @@ export interface TextureOption {
804
884
  * them into the system's instancing geometry.
805
885
  */
806
886
  export interface GeometryOption {
887
+ /** Its label in the menu. */
807
888
  label: string;
889
+ /** Vertex positions, three values per vertex. */
808
890
  positions: Float32Array | number[];
891
+ /** Triangle indices; without them the vertices are drawn in sequence. */
809
892
  indices?: Uint32Array | number[];
893
+ /** The first UV set, two values per vertex; without it the mesh samples UV (0, 0). */
810
894
  uvs?: Float32Array | number[];
895
+ /** A second UV set (glTF `TEXCOORD_1`), what textures on UV channel 1 read on the mesh. */
896
+ uvs2?: Float32Array | number[];
897
+ /** Vertex normals, three values per vertex; without them lighting uses +Z. */
811
898
  normals?: Float32Array | number[];
812
899
  }
813
900
 
@@ -900,6 +987,7 @@ function MeshPreview({
900
987
  );
901
988
  }
902
989
 
990
+ /** The Renderer module (Unity's Renderer): render mode, material, textures, sorting and mesh settings. */
903
991
  export function RendererModule({
904
992
  binding,
905
993
  textureOptions,
@@ -909,13 +997,19 @@ export function RendererModule({
909
997
  }: ModuleProps & {
910
998
  textureOptions?: TextureOption[];
911
999
  geometryOptions?: GeometryOption[];
912
- resolveTexture?: (url: string) => unknown;
1000
+ resolveTexture?: (url: string) => Texture | null | undefined;
913
1001
  resolveGeometry?: (file: File) => Promise<GeometryData>;
914
1002
  }) {
915
1003
  const system = binding.system;
916
- const currentTextureUrl = (system.texture as {url?: string} | null)?.url ?? '';
917
- const [promptingTextureUrl, setPromptingTextureUrl] = useState(false);
918
1004
  const materialLabel = getMaterialLabel(system.material);
1005
+ const isMesh = system.renderMode === RenderMode.Mesh;
1006
+ const gradientMap = system.gradientMap;
1007
+ const erosion = system.erosion;
1008
+ const distortion = system.uvDistortion;
1009
+ const patchErosion = (patch: ParticleErosionOptions) =>
1010
+ binding.apply((s) => (s.erosion = {...s.erosion, ...patch}));
1011
+ const patchDistortion = (patch: ParticleUVDistortionOptions) =>
1012
+ binding.apply((s) => (s.uvDistortion = {...s.uvDistortion, ...patch}));
919
1013
  const loadTexture = useMemo(() => {
920
1014
  return (url: string) => {
921
1015
  if (resolveTexture) {
@@ -993,18 +1087,112 @@ export function RendererModule({
993
1087
  />
994
1088
  </Row>
995
1089
  ))}
996
- <Row label="Head width scale">
1090
+ <Row label="Tessellation">
1091
+ <NumberField
1092
+ value={(system.rendererEmitterSettings as {tessellation?: number}).tessellation ?? 0}
1093
+ min={0}
1094
+ max={16}
1095
+ step={1}
1096
+ onChange={(v) =>
1097
+ binding.apply((s) => {
1098
+ (s.rendererEmitterSettings as {tessellation?: number}).tessellation = Math.round(v);
1099
+ })
1100
+ }
1101
+ />
1102
+ </Row>
1103
+ <Row label="Curve tension">
1104
+ <NumberField
1105
+ value={(system.rendererEmitterSettings as {curveTension?: number}).curveTension ?? 0}
1106
+ min={0}
1107
+ max={1}
1108
+ step={0.05}
1109
+ onChange={(v) =>
1110
+ binding.apply((s) => {
1111
+ (s.rendererEmitterSettings as {curveTension?: number}).curveTension = v;
1112
+ })
1113
+ }
1114
+ />
1115
+ </Row>
1116
+ <Row label="Texture mode">
1117
+ <SelectField
1118
+ value={(system.rendererEmitterSettings as TrailSettings).textureMode ?? 'stretch'}
1119
+ options={[
1120
+ {value: 'stretch', label: 'Stretch (once, by distance)'},
1121
+ {value: 'distributePerSegment', label: 'Distribute per segment'},
1122
+ {value: 'tile', label: 'Tile (per world unit)'},
1123
+ {value: 'repeatPerSegment', label: 'Repeat per segment'},
1124
+ ]}
1125
+ onChange={(mode) =>
1126
+ binding.apply((s) => {
1127
+ (s.rendererEmitterSettings as TrailSettings).textureMode =
1128
+ mode === 'stretch' ? undefined : mode;
1129
+ })
1130
+ }
1131
+ />
1132
+ </Row>
1133
+ <Row label="Texture scale">
1134
+ <NumberField
1135
+ value={(system.rendererEmitterSettings as TrailSettings).textureScale ?? 1}
1136
+ min={0.01}
1137
+ step={0.1}
1138
+ onChange={(v) =>
1139
+ binding.apply((s) => {
1140
+ (s.rendererEmitterSettings as TrailSettings).textureScale = v === 1 ? undefined : v;
1141
+ })
1142
+ }
1143
+ />
1144
+ </Row>
1145
+ <Row label="Min vertex distance">
997
1146
  <NumberField
998
- value={(system.rendererEmitterSettings as {headWidthScale?: number}).headWidthScale ?? 1}
1147
+ value={(system.rendererEmitterSettings as TrailSettings).minVertexDistance ?? 0}
1148
+ min={0}
1149
+ step={0.05}
1150
+ onChange={(v) =>
1151
+ binding.apply((s) => {
1152
+ (s.rendererEmitterSettings as TrailSettings).minVertexDistance =
1153
+ v > 0 ? v : undefined;
1154
+ })
1155
+ }
1156
+ />
1157
+ </Row>
1158
+ <Row label="Trail time">
1159
+ <NumberField
1160
+ value={(system.rendererEmitterSettings as TrailSettings).lifetime ?? 0}
999
1161
  min={0}
1000
1162
  step={0.1}
1001
1163
  onChange={(v) =>
1002
1164
  binding.apply((s) => {
1003
- (s.rendererEmitterSettings as {headWidthScale?: number}).headWidthScale = v;
1165
+ (s.rendererEmitterSettings as TrailSettings).lifetime = v > 0 ? v : undefined;
1166
+ })
1167
+ }
1168
+ />
1169
+ </Row>
1170
+ <Row label="Swept trail">
1171
+ <CheckboxField
1172
+ value={!!(system.rendererEmitterSettings as TrailSettings).sideAxis}
1173
+ onChange={(v) =>
1174
+ binding.apply((s) => {
1175
+ (s.rendererEmitterSettings as TrailSettings).sideAxis = v
1176
+ ? new Vector3(0, 1, 0)
1177
+ : undefined;
1004
1178
  })
1005
1179
  }
1006
1180
  />
1007
1181
  </Row>
1182
+ {(system.rendererEmitterSettings as TrailSettings).sideAxis && (
1183
+ <Row label="Side axis">
1184
+ <AxisField
1185
+ value={(system.rendererEmitterSettings as TrailSettings).sideAxis!}
1186
+ onChange={(axis) =>
1187
+ binding.apply((s) => {
1188
+ // The axis's length scales the width: keep it a unit vector.
1189
+ (s.rendererEmitterSettings as TrailSettings).sideAxis =
1190
+ axis.length() > 0 ? axis.normalize() : axis;
1191
+ })
1192
+ }
1193
+ />
1194
+ </Row>
1195
+ )}
1008
1196
  </>
1009
1197
  )}
1010
1198
  {system.renderMode === RenderMode.StretchedBillBoard && (
@@ -1033,26 +1221,7 @@ export function RendererModule({
1033
1221
  </Row>
1034
1222
  </>
1035
1223
  )}
1036
- <div
1037
- style={{
1038
- marginTop: 10,
1039
- marginBottom: 2,
1040
- fontSize: 11,
1041
- fontWeight: 600,
1042
- color: '#7c8db5',
1043
- textTransform: 'uppercase',
1044
- letterSpacing: 0.4,
1045
- display: 'flex',
1046
- alignItems: 'center',
1047
- gap: 5,
1048
- }}
1049
- >
1050
- Material
1051
- <InspectorInfoIcon
1052
- content="Shader and blending settings applied to the particle sprite or mesh."
1053
- size={12}
1054
- />
1055
- </div>
1224
+ <SubHeader title="Material" hint="Shader and blending settings applied to the particle sprite or mesh." />
1056
1225
  {materialLabel ? (
1057
1226
  <Row label="Source">
1058
1227
  <div style={{fontSize: 12, color: theme.textDim, lineHeight: 1.45}}>{materialLabel}</div>
@@ -1077,93 +1246,41 @@ export function RendererModule({
1077
1246
  />
1078
1247
  </Row>
1079
1248
  <Row label="Texture">
1080
- <SelectField
1081
- value={
1082
- !currentTextureUrl
1083
- ? '__none'
1084
- : (textureOptions ?? []).some((o) => o.url === currentTextureUrl)
1085
- ? currentTextureUrl
1086
- : '__current'
1087
- }
1088
- options={[
1089
- {value: '__none', label: '(none)'},
1090
- ...(currentTextureUrl && !(textureOptions ?? []).some((o) => o.url === currentTextureUrl)
1091
- ? [{value: '__current', label: `(current) ${currentTextureUrl.slice(-24)}`}]
1092
- : []),
1093
- ...(textureOptions ?? []).map((o) => ({value: o.url, label: o.label})),
1094
- {value: '__url', label: 'Custom URL…'},
1095
- {value: '__file', label: 'Load from file…'},
1096
- ]}
1097
- onChange={(url) => {
1098
- if (url === '__current') {
1099
- return;
1100
- }
1101
- if (url === '__none') {
1102
- patchMaterial({texture: null});
1103
- return;
1104
- }
1105
- if (url === '__url') {
1106
- setPromptingTextureUrl(true);
1107
- return;
1108
- }
1109
- if (url === '__file') {
1110
- const input = document.createElement('input');
1111
- input.type = 'file';
1112
- input.accept = 'image/*';
1113
- input.onchange = () => {
1114
- const file = input.files?.[0];
1115
- if (!file) {
1116
- return;
1117
- }
1118
- const reader = new FileReader();
1119
- reader.onload = () => {
1120
- const texture = loadTexture(reader.result as string);
1121
- if (texture) {
1122
- patchMaterial({texture});
1123
- }
1124
- };
1125
- reader.readAsDataURL(file);
1126
- };
1127
- input.click();
1128
- return;
1129
- }
1130
- const texture = loadTexture(url);
1131
- if (texture) {
1132
- patchMaterial({texture});
1133
- }
1134
- }}
1249
+ <TextureSelect
1250
+ value={system.texture}
1251
+ textureOptions={textureOptions}
1252
+ loadTexture={loadTexture}
1253
+ onChange={(texture) => patchMaterial({texture})}
1254
+ preview
1135
1255
  />
1136
- {promptingTextureUrl && (
1137
- <PromptDialog
1138
- title="Texture URL"
1139
- defaultValue={currentTextureUrl}
1140
- placeholder="https://…"
1141
- onSubmit={(url) => {
1142
- const texture = loadTexture(url);
1143
- if (texture) {
1144
- patchMaterial({texture});
1145
- }
1146
- setPromptingTextureUrl(false);
1147
- }}
1148
- onCancel={() => setPromptingTextureUrl(false)}
1149
- />
1150
- )}
1151
- {currentTextureUrl && (
1152
- <img
1153
- src={currentTextureUrl}
1154
- alt="texture preview"
1155
- style={{
1156
- width: 64,
1157
- height: 64,
1158
- marginTop: 6,
1159
- objectFit: 'contain',
1160
- borderRadius: 6,
1161
- border: '1px solid #2b3761',
1162
- background: 'repeating-conic-gradient(#2a3252 0% 25%, #171d33 0% 50%) 0 0 / 12px 12px',
1163
- }}
1164
- />
1165
- )}
1166
1256
  </Row>
1257
+ <TextureUVField
1258
+ labelPrefix="Map "
1259
+ value={system.mapUV ?? normalizeTextureUV(undefined)}
1260
+ onChange={(uv) => binding.apply((s) => (s.mapUV = uv))}
1261
+ />
1262
+ {!isMesh && (
1263
+ <Row label="Flip U/V">
1264
+ <div style={{display: 'flex', gap: 6}}>
1265
+ {(['x', 'y'] as const).map((axis) => (
1266
+ <NumberField
1267
+ key={axis}
1268
+ value={system.flip?.[axis] ?? 0}
1269
+ min={0}
1270
+ max={1}
1271
+ step={0.1}
1272
+ onChange={(v) =>
1273
+ binding.apply((s) => {
1274
+ const next = s.flip?.clone() ?? new Vector2(0, 0);
1275
+ next[axis] = Math.min(1, Math.max(0, v));
1276
+ s.flip = next.x === 0 && next.y === 0 ? undefined : next;
1277
+ })
1278
+ }
1279
+ />
1280
+ ))}
1281
+ </div>
1282
+ </Row>
1283
+ )}
1167
1284
  <Row label="Alpha test">
1168
1285
  <NumberField
1169
1286
  value={system.getRendererSettings().materialAlphaTest ?? 0}
@@ -1184,6 +1301,134 @@ export function RendererModule({
1184
1301
  onChange={(v) => patchMaterial({depthTest: v})}
1185
1302
  />
1186
1303
  </Row>
1304
+ <SubHeader
1305
+ title="Gradient map"
1306
+ hint="Recolours the map through a colour ramp: one channel of the map, or its luminance, picks a colour along the ramp."
1307
+ />
1308
+ <Row label="Ramp">
1309
+ <TextureSelect
1310
+ value={gradientMap?.ramp}
1311
+ textureOptions={textureOptions}
1312
+ loadTexture={loadTexture}
1313
+ promptTitle="Ramp texture URL"
1314
+ onChange={(ramp) =>
1315
+ binding.apply((s) => (s.gradientMap = ramp ? {...s.gradientMap, ramp} : undefined))
1316
+ }
1317
+ />
1318
+ </Row>
1319
+ {gradientMap && (
1320
+ <Row label="Ramp channel">
1321
+ <SelectField<GradientMapChannel>
1322
+ value={gradientMap.channel}
1323
+ options={[
1324
+ {value: 'r', label: 'Red'},
1325
+ {value: 'g', label: 'Green'},
1326
+ {value: 'b', label: 'Blue'},
1327
+ {value: 'a', label: 'Alpha (ramp alpha replaces)'},
1328
+ {value: 'luminance', label: 'Luminance'},
1329
+ ]}
1330
+ onChange={(channel) => binding.apply((s) => (s.gradientMap = {...s.gradientMap, channel}))}
1331
+ />
1332
+ </Row>
1333
+ )}
1334
+ <SubHeader
1335
+ title="Erosion"
1336
+ hint="Alpha decides how much of the shape is left instead of how transparent it is, so a fading alpha dissolves or wipes the shape away."
1337
+ />
1338
+ <Row label="Erosion">
1339
+ <CheckboxField
1340
+ value={!!erosion}
1341
+ onChange={(v) => binding.apply((s) => (s.erosion = v ? {} : undefined))}
1342
+ />
1343
+ </Row>
1344
+ {erosion && (
1345
+ <>
1346
+ <Row label="Erosion mask">
1347
+ <TextureSelect
1348
+ value={erosion.mask}
1349
+ textureOptions={textureOptions}
1350
+ loadTexture={loadTexture}
1351
+ noneLabel="(map alpha)"
1352
+ promptTitle="Erosion mask URL"
1353
+ onChange={(mask) => patchErosion({mask})}
1354
+ />
1355
+ </Row>
1356
+ {erosion.mask && (
1357
+ <TextureUVField
1358
+ labelPrefix="Mask "
1359
+ value={erosion.maskUV}
1360
+ onChange={(maskUV) => patchErosion({maskUV})}
1361
+ />
1362
+ )}
1363
+ <Row label="Softness">
1364
+ <NumberField
1365
+ value={erosion.softness}
1366
+ min={0.0001}
1367
+ step={0.01}
1368
+ onChange={(softness) => patchErosion({softness})}
1369
+ />
1370
+ </Row>
1371
+ <Row label="Edge width">
1372
+ <NumberField
1373
+ value={erosion.edgeWidth}
1374
+ min={0}
1375
+ step={0.01}
1376
+ onChange={(edgeWidth) => patchErosion({edgeWidth})}
1377
+ />
1378
+ </Row>
1379
+ {(erosion.edgeWidth > 0 || erosion.edgeColor.some((c) => c !== 1)) && (
1380
+ <Row label="Edge color">
1381
+ <div style={{display: 'flex', gap: 6}}>
1382
+ {[0, 1, 2].map((channel) => (
1383
+ <NumberField
1384
+ key={channel}
1385
+ value={erosion.edgeColor[channel]}
1386
+ min={0}
1387
+ step={0.05}
1388
+ onChange={(v) => {
1389
+ const edgeColor: [number, number, number] = [...erosion.edgeColor];
1390
+ edgeColor[channel] = v;
1391
+ patchErosion({edgeColor});
1392
+ }}
1393
+ />
1394
+ ))}
1395
+ </div>
1396
+ </Row>
1397
+ )}
1398
+ </>
1399
+ )}
1400
+ <SubHeader
1401
+ title="UV distortion"
1402
+ hint="A scrolling noise texture pushes the map's UV about, for heat shimmer and licking, breathing shapes."
1403
+ />
1404
+ <Row label="Noise texture">
1405
+ <TextureSelect
1406
+ value={distortion?.map}
1407
+ textureOptions={textureOptions}
1408
+ loadTexture={loadTexture}
1409
+ promptTitle="Distortion noise URL"
1410
+ onChange={(map) =>
1411
+ binding.apply((s) => (s.uvDistortion = map ? {...s.uvDistortion, map} : undefined))
1412
+ }
1413
+ />
1414
+ </Row>
1415
+ {distortion && (
1416
+ <>
1417
+ <Row label="Distortion strength">
1418
+ <NumberField
1419
+ value={distortion.strength}
1420
+ min={0}
1421
+ step={0.01}
1422
+ onChange={(strength) => patchDistortion({strength})}
1423
+ />
1424
+ </Row>
1425
+ <TextureUVField
1426
+ labelPrefix="Noise "
1427
+ value={distortion.mapUV}
1428
+ onChange={(mapUV) => patchDistortion({mapUV})}
1429
+ />
1430
+ </>
1431
+ )}
1187
1432
  {system.renderMode === RenderMode.Mesh &&
1188
1433
  (() => {
1189
1434
  const preset = detectGeometryPreset(system.instancingGeometry);
@@ -1192,11 +1437,20 @@ export function RendererModule({
1192
1437
  const hostGeometry = geometryOptions ?? [];
1193
1438
  const applyGeometry = (g: GeometryData) =>
1194
1439
  binding.apply((s) => {
1440
+ // The previous mesh's sets go first, so the positions setter gives
1441
+ // this mesh the library's own fallback UVs and normals; then every set
1442
+ // this mesh has. An unindexed mesh draws its vertices in sequence.
1195
1443
  const settings = s.getRendererSettings();
1196
- settings.instancingIndices = (g.indices ? new Uint32Array(g.indices) : undefined) as never;
1197
- settings.instancingUVs = (g.uvs ? new Float32Array(g.uvs) : undefined) as never;
1198
- settings.instancingNormals = (g.normals ? new Float32Array(g.normals) : undefined) as never;
1444
+ settings.instancingUVs = undefined;
1445
+ settings.instancingUVs2 = undefined;
1446
+ settings.instancingNormals = undefined;
1199
1447
  s.instancingGeometry = new Float32Array(g.positions);
1448
+ s.instancingIndices = g.indices
1449
+ ? new Uint32Array(g.indices)
1450
+ : Uint32Array.from({length: g.positions.length / 3}, (_, i) => i);
1451
+ if (g.uvs) s.instancingUVs = new Float32Array(g.uvs);
1452
+ if (g.uvs2) s.instancingUVs2 = new Float32Array(g.uvs2);
1453
+ if (g.normals) settings.instancingNormals = new Float32Array(g.normals);
1200
1454
  });
1201
1455
  return (
1202
1456
  <Row label="Geometry">
@@ -1233,6 +1487,7 @@ export function RendererModule({
1233
1487
  }
1234
1488
  binding.apply((s) => {
1235
1489
  const settings = s.getRendererSettings();
1490
+ settings.instancingUVs2 = undefined;
1236
1491
  if (next === 'cube') {
1237
1492
  settings.instancingIndices = MESH_CUBE_INDICES;
1238
1493
  settings.instancingUVs = undefined;
@@ -1252,7 +1507,9 @@ export function RendererModule({
1252
1507
  />
1253
1508
  {preset === 'custom' && (
1254
1509
  <span style={{fontSize: 10.5, color: '#9eb9ff'}}>
1255
- {vertexCount} verts / {triangleCount} tris — picking a preset replaces this mesh
1510
+ {vertexCount} verts / {triangleCount} tris
1511
+ {system.getRendererSettings().instancingUVs2 ? ' / 2 UV sets' : ''} — picking a
1512
+ preset replaces this mesh
1256
1513
  </span>
1257
1514
  )}
1258
1515
  </div>
@@ -1414,6 +1671,19 @@ export function RendererModule({
1414
1671
  </Row>
1415
1672
  </>
1416
1673
  )}
1674
+ {isMesh && (
1675
+ <Row label="Render face">
1676
+ <SelectField
1677
+ value={system.renderFace}
1678
+ options={[
1679
+ {value: 'both', label: 'Both'},
1680
+ {value: 'front', label: 'Front'},
1681
+ {value: 'back', label: 'Back'},
1682
+ ]}
1683
+ onChange={(face) => binding.apply((s) => (s.renderFace = face as RenderFace))}
1684
+ />
1685
+ </Row>
1686
+ )}
1417
1687
  <Row label="Sort mode">
1418
1688
  <SelectField
1419
1689
  value={system.sortMode}