babylon.quarks-editor 0.25.0 → 0.27.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 (151) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +38 -5
  3. package/dist/core/assetDragSession.d.ts +1 -1
  4. package/dist/core/assetTree.d.ts +1 -1
  5. package/dist/core/binding.d.ts +2 -2
  6. package/dist/core/binding.js +1 -1
  7. package/dist/core/defaultEffect.d.ts +2 -2
  8. package/dist/core/defaultEffect.js +3 -3
  9. package/dist/core/editorScene.d.ts +1 -1
  10. package/dist/core/effectTree.d.ts +1 -1
  11. package/dist/core/effectTree.js +13 -88
  12. package/dist/core/emitterShapeWireframe.d.ts +2 -2
  13. package/dist/core/emitterShapeWireframe.js +45 -13
  14. package/dist/core/galleryDropMarker.d.ts +2 -2
  15. package/dist/core/galleryDropMarker.js +6 -6
  16. package/dist/core/galleryLayout.d.ts +3 -3
  17. package/dist/core/galleryLayout.js +1 -1
  18. package/dist/core/galleryPlayback.d.ts +1 -1
  19. package/dist/core/galleryScene.d.ts +4 -4
  20. package/dist/core/galleryScene.js +3 -3
  21. package/dist/core/gallerySelectionMarker.d.ts +4 -4
  22. package/dist/core/gallerySelectionMarker.js +5 -5
  23. package/dist/core/groups.d.ts +1 -1
  24. package/dist/core/groups.js +1 -1
  25. package/dist/core/history.d.ts +1 -1
  26. package/dist/core/indexedDbProjectStore.d.ts +1 -1
  27. package/dist/core/lineShaders.d.ts +2 -2
  28. package/dist/core/lineShaders.js +2 -2
  29. package/dist/core/loadEffect.d.ts +2 -2
  30. package/dist/core/loadEffect.js +1 -1
  31. package/dist/core/pickAssetFiles.js +1 -1
  32. package/dist/core/sceneEntries.d.ts +4 -4
  33. package/dist/core/sceneEntries.js +2 -2
  34. package/dist/core/scrub.d.ts +1 -1
  35. package/dist/core/shapes.d.ts +1 -1
  36. package/dist/core/systems.d.ts +2 -2
  37. package/dist/core/textureExport.d.ts +1 -1
  38. package/dist/core/timeline.d.ts +2 -2
  39. package/dist/core/transport.d.ts +3 -3
  40. package/dist/core/transport.js +2 -2
  41. package/dist/core/viewportPicker.d.ts +4 -4
  42. package/dist/core/viewportPicker.js +8 -7
  43. package/dist/index.d.ts +21 -21
  44. package/dist/index.js +12 -12
  45. package/dist/react/AssetsPanel.d.ts +2 -2
  46. package/dist/react/AssetsPanel.js +6 -6
  47. package/dist/react/ChoiceDialog.js +1 -1
  48. package/dist/react/ContextMenu.js +1 -1
  49. package/dist/react/CurveEditor.d.ts +1 -1
  50. package/dist/react/CurveEditor.js +2 -2
  51. package/dist/react/EditorWorkspace.d.ts +2 -2
  52. package/dist/react/EditorWorkspace.js +4 -4
  53. package/dist/react/EffectEditor.d.ts +3 -3
  54. package/dist/react/EffectEditor.js +7 -7
  55. package/dist/react/EffectEditorHost.d.ts +6 -6
  56. package/dist/react/EffectEditorHost.js +59 -62
  57. package/dist/react/EffectTreeView.d.ts +1 -1
  58. package/dist/react/EffectTreeView.js +3 -3
  59. package/dist/react/GradientEditor.d.ts +1 -1
  60. package/dist/react/GradientEditor.js +3 -3
  61. package/dist/react/InspectorInfoIcon.js +3 -3
  62. package/dist/react/InspectorLabel.js +3 -3
  63. package/dist/react/MessageDialog.js +1 -1
  64. package/dist/react/ModuleSection.js +4 -4
  65. package/dist/react/ObjectModule.d.ts +1 -1
  66. package/dist/react/ObjectModule.js +4 -4
  67. package/dist/react/ProjectsPage.d.ts +1 -1
  68. package/dist/react/ProjectsPage.js +6 -6
  69. package/dist/react/PromptDialog.js +1 -1
  70. package/dist/react/ScenePanel.d.ts +2 -2
  71. package/dist/react/ScenePanel.js +5 -5
  72. package/dist/react/SubEmittersModule.d.ts +1 -1
  73. package/dist/react/SubEmittersModule.js +5 -5
  74. package/dist/react/TextureSelect.d.ts +24 -0
  75. package/dist/react/TextureSelect.js +74 -0
  76. package/dist/react/TextureUVField.d.ts +14 -0
  77. package/dist/react/TextureUVField.js +23 -0
  78. package/dist/react/TimelinePanel.d.ts +7 -7
  79. package/dist/react/TimelinePanel.js +11 -9
  80. package/dist/react/TimelineTrackRow.d.ts +3 -3
  81. package/dist/react/TimelineTrackRow.js +6 -6
  82. package/dist/react/TransportBar.d.ts +4 -4
  83. package/dist/react/TransportBar.js +3 -3
  84. package/dist/react/ValueField.d.ts +1 -1
  85. package/dist/react/ValueField.js +3 -3
  86. package/dist/react/behaviorModules.d.ts +4 -2
  87. package/dist/react/behaviorModules.js +52 -16
  88. package/dist/react/fields.d.ts +1 -0
  89. package/dist/react/fields.js +3 -3
  90. package/dist/react/index.d.ts +40 -38
  91. package/dist/react/index.js +25 -23
  92. package/dist/react/inspectorHints.js +31 -0
  93. package/dist/react/modules.d.ts +3 -1
  94. package/dist/react/modules.js +100 -100
  95. package/package.json +3 -2
  96. package/src/core/assetDragSession.ts +1 -1
  97. package/src/core/assetTree.ts +1 -1
  98. package/src/core/binding.ts +3 -3
  99. package/src/core/defaultEffect.ts +4 -4
  100. package/src/core/editorScene.ts +1 -1
  101. package/src/core/effectTree.ts +13 -101
  102. package/src/core/emitterShapeWireframe.ts +48 -16
  103. package/src/core/galleryDropMarker.ts +8 -8
  104. package/src/core/galleryLayout.ts +4 -4
  105. package/src/core/galleryPlayback.ts +1 -1
  106. package/src/core/galleryScene.ts +5 -5
  107. package/src/core/gallerySelectionMarker.ts +7 -7
  108. package/src/core/groups.ts +1 -1
  109. package/src/core/history.ts +1 -1
  110. package/src/core/indexedDbProjectStore.ts +1 -1
  111. package/src/core/lineShaders.ts +2 -2
  112. package/src/core/loadEffect.ts +3 -3
  113. package/src/core/pickAssetFiles.ts +2 -2
  114. package/src/core/sceneEntries.ts +5 -5
  115. package/src/core/scrub.ts +1 -1
  116. package/src/core/shapes.ts +1 -1
  117. package/src/core/systems.ts +2 -2
  118. package/src/core/textureExport.ts +1 -1
  119. package/src/core/timeline.ts +3 -3
  120. package/src/core/transport.ts +4 -4
  121. package/src/core/viewportPicker.ts +12 -11
  122. package/src/index.ts +21 -21
  123. package/src/react/AssetsPanel.tsx +6 -6
  124. package/src/react/ChoiceDialog.tsx +1 -1
  125. package/src/react/ContextMenu.tsx +1 -1
  126. package/src/react/CurveEditor.tsx +2 -2
  127. package/src/react/EditorWorkspace.tsx +5 -5
  128. package/src/react/EffectEditor.tsx +10 -8
  129. package/src/react/EffectEditorHost.tsx +64 -69
  130. package/src/react/EffectTreeView.tsx +4 -4
  131. package/src/react/GradientEditor.tsx +3 -3
  132. package/src/react/InspectorInfoIcon.tsx +3 -3
  133. package/src/react/InspectorLabel.tsx +3 -3
  134. package/src/react/MessageDialog.tsx +1 -1
  135. package/src/react/ModuleSection.tsx +4 -4
  136. package/src/react/ObjectModule.tsx +5 -5
  137. package/src/react/ProjectsPage.tsx +6 -6
  138. package/src/react/PromptDialog.tsx +1 -1
  139. package/src/react/ScenePanel.tsx +7 -7
  140. package/src/react/SubEmittersModule.tsx +6 -6
  141. package/src/react/TextureSelect.tsx +118 -0
  142. package/src/react/TextureUVField.tsx +62 -0
  143. package/src/react/TimelinePanel.tsx +17 -15
  144. package/src/react/TimelineTrackRow.tsx +9 -9
  145. package/src/react/TransportBar.tsx +7 -7
  146. package/src/react/ValueField.tsx +3 -3
  147. package/src/react/behaviorModules.tsx +211 -28
  148. package/src/react/fields.tsx +10 -3
  149. package/src/react/index.ts +41 -38
  150. package/src/react/inspectorHints.ts +40 -0
  151. package/src/react/modules.tsx +376 -131
@@ -1,7 +1,19 @@
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
+ ScalingMode,
14
+ SortMode,
15
+ TrailSettings,
16
+ } from 'babylon.quarks';
5
17
  import {
6
18
  AxisAngleGenerator,
7
19
  BoxEmitFrom,
@@ -17,12 +29,14 @@ import {
17
29
  RandomQuatGenerator,
18
30
  RenderMode,
19
31
  SizeOverLife,
32
+ Vector2,
20
33
  Vector3,
21
34
  Vector3Function,
22
35
  Vector4,
36
+ normalizeTextureUV,
23
37
  } from 'babylon.quarks';
24
- import React, {useMemo, useState} from 'react';
25
- import {EffectBinding} from '../core/binding';
38
+ import React, {useMemo} from 'react';
39
+ import {EffectBinding} from '../core/binding.js';
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,17 +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';
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} 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';
55
70
 
56
71
  interface ModuleProps {
57
72
  binding: EffectBinding;
@@ -105,6 +120,49 @@ const PARAM_LABELS: {[key: string]: string} = {
105
120
  speed: 'Speed (lengths/s)',
106
121
  };
107
122
 
123
+ /** Uppercase group title inside a module, as the Renderer's "Material" heading. */
124
+ function SubHeader(props: {title: string; hint: string}) {
125
+ return (
126
+ <div
127
+ style={{
128
+ marginTop: 10,
129
+ marginBottom: 2,
130
+ fontSize: 11,
131
+ fontWeight: 600,
132
+ color: '#7c8db5',
133
+ textTransform: 'uppercase',
134
+ letterSpacing: 0.4,
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ gap: 5,
138
+ }}
139
+ >
140
+ {props.title}
141
+ <InspectorInfoIcon content={props.hint} size={12} />
142
+ </div>
143
+ );
144
+ }
145
+
146
+ /** Three number inputs editing a Vector3 by value. */
147
+ function AxisField(props: {value: Vector3; onChange: (next: Vector3) => void}) {
148
+ return (
149
+ <div style={{display: 'flex', gap: 6}}>
150
+ {(['x', 'y', 'z'] as const).map((axis) => (
151
+ <NumberField
152
+ key={axis}
153
+ value={props.value[axis]}
154
+ step={0.1}
155
+ onChange={(v) => {
156
+ const next = props.value.clone();
157
+ next[axis] = v;
158
+ props.onChange(next);
159
+ }}
160
+ />
161
+ ))}
162
+ </div>
163
+ );
164
+ }
165
+
108
166
  function ColorInput(props: {value: Vector4; onChange: (next: Vector4) => void}) {
109
167
  const {value, onChange} = props;
110
168
  return (
@@ -205,9 +263,31 @@ export function MainModule({binding}: ModuleProps) {
205
263
  <Row label="World space">
206
264
  <CheckboxField value={system.worldSpace} onChange={(v) => binding.apply((s) => (s.worldSpace = v))} />
207
265
  </Row>
266
+ {system.worldSpace && (
267
+ <Row label="Scaling mode">
268
+ <SelectField<ScalingMode>
269
+ value={system.scalingMode}
270
+ options={[
271
+ {value: 'shape', label: 'Shape (sizes and shape only)'},
272
+ {value: 'hierarchy', label: 'Hierarchy (motion scales too)'},
273
+ ]}
274
+ onChange={(mode) => binding.apply((s) => (s.scalingMode = mode))}
275
+ />
276
+ </Row>
277
+ )}
208
278
  <Row label="Prewarm">
209
279
  <CheckboxField value={system.prewarm} onChange={(v) => binding.apply((s) => (s.prewarm = v))} />
210
280
  </Row>
281
+ {system.prewarm && (
282
+ <Row label="Warm-up time">
283
+ <NumberField
284
+ value={system.prewarmTime}
285
+ step={0.1}
286
+ min={0}
287
+ onChange={(v) => binding.apply((s) => (s.prewarmTime = Math.max(0, v)))}
288
+ />
289
+ </Row>
290
+ )}
211
291
  <Row label="Playback speed">
212
292
  <NumberField
213
293
  value={system.playbackSpeed}
@@ -808,6 +888,8 @@ export interface GeometryOption {
808
888
  positions: Float32Array | number[];
809
889
  indices?: Uint32Array | number[];
810
890
  uvs?: Float32Array | number[];
891
+ /** A second UV set (glTF `TEXCOORD_1`), what textures on UV channel 1 read on the mesh. */
892
+ uvs2?: Float32Array | number[];
811
893
  normals?: Float32Array | number[];
812
894
  }
813
895
 
@@ -913,9 +995,15 @@ export function RendererModule({
913
995
  resolveGeometry?: (file: File) => Promise<GeometryData>;
914
996
  }) {
915
997
  const system = binding.system;
916
- const currentTextureUrl = (system.texture as {url?: string} | null)?.url ?? '';
917
- const [promptingTextureUrl, setPromptingTextureUrl] = useState(false);
918
998
  const materialLabel = getMaterialLabel(system.material);
999
+ const isMesh = system.renderMode === RenderMode.Mesh;
1000
+ const gradientMap = system.gradientMap;
1001
+ const erosion = system.erosion;
1002
+ const distortion = system.uvDistortion;
1003
+ const patchErosion = (patch: ParticleErosionOptions) =>
1004
+ binding.apply((s) => (s.erosion = {...s.erosion, ...patch}));
1005
+ const patchDistortion = (patch: ParticleUVDistortionOptions) =>
1006
+ binding.apply((s) => (s.uvDistortion = {...s.uvDistortion, ...patch}));
919
1007
  const loadTexture = useMemo(() => {
920
1008
  return (url: string) => {
921
1009
  if (resolveTexture) {
@@ -993,18 +1081,112 @@ export function RendererModule({
993
1081
  />
994
1082
  </Row>
995
1083
  ))}
996
- <Row label="Head width scale">
1084
+ <Row label="Tessellation">
1085
+ <NumberField
1086
+ value={(system.rendererEmitterSettings as {tessellation?: number}).tessellation ?? 0}
1087
+ min={0}
1088
+ max={16}
1089
+ step={1}
1090
+ onChange={(v) =>
1091
+ binding.apply((s) => {
1092
+ (s.rendererEmitterSettings as {tessellation?: number}).tessellation = Math.round(v);
1093
+ })
1094
+ }
1095
+ />
1096
+ </Row>
1097
+ <Row label="Curve tension">
1098
+ <NumberField
1099
+ value={(system.rendererEmitterSettings as {curveTension?: number}).curveTension ?? 0}
1100
+ min={0}
1101
+ max={1}
1102
+ step={0.05}
1103
+ onChange={(v) =>
1104
+ binding.apply((s) => {
1105
+ (s.rendererEmitterSettings as {curveTension?: number}).curveTension = v;
1106
+ })
1107
+ }
1108
+ />
1109
+ </Row>
1110
+ <Row label="Texture mode">
1111
+ <SelectField
1112
+ value={(system.rendererEmitterSettings as TrailSettings).textureMode ?? 'stretch'}
1113
+ options={[
1114
+ {value: 'stretch', label: 'Stretch (once, by distance)'},
1115
+ {value: 'distributePerSegment', label: 'Distribute per segment'},
1116
+ {value: 'tile', label: 'Tile (per world unit)'},
1117
+ {value: 'repeatPerSegment', label: 'Repeat per segment'},
1118
+ ]}
1119
+ onChange={(mode) =>
1120
+ binding.apply((s) => {
1121
+ (s.rendererEmitterSettings as TrailSettings).textureMode =
1122
+ mode === 'stretch' ? undefined : mode;
1123
+ })
1124
+ }
1125
+ />
1126
+ </Row>
1127
+ <Row label="Texture scale">
1128
+ <NumberField
1129
+ value={(system.rendererEmitterSettings as TrailSettings).textureScale ?? 1}
1130
+ min={0.01}
1131
+ step={0.1}
1132
+ onChange={(v) =>
1133
+ binding.apply((s) => {
1134
+ (s.rendererEmitterSettings as TrailSettings).textureScale = v === 1 ? undefined : v;
1135
+ })
1136
+ }
1137
+ />
1138
+ </Row>
1139
+ <Row label="Min vertex distance">
997
1140
  <NumberField
998
- value={(system.rendererEmitterSettings as {headWidthScale?: number}).headWidthScale ?? 1}
1141
+ value={(system.rendererEmitterSettings as TrailSettings).minVertexDistance ?? 0}
1142
+ min={0}
1143
+ step={0.05}
1144
+ onChange={(v) =>
1145
+ binding.apply((s) => {
1146
+ (s.rendererEmitterSettings as TrailSettings).minVertexDistance =
1147
+ v > 0 ? v : undefined;
1148
+ })
1149
+ }
1150
+ />
1151
+ </Row>
1152
+ <Row label="Trail time">
1153
+ <NumberField
1154
+ value={(system.rendererEmitterSettings as TrailSettings).lifetime ?? 0}
999
1155
  min={0}
1000
1156
  step={0.1}
1001
1157
  onChange={(v) =>
1002
1158
  binding.apply((s) => {
1003
- (s.rendererEmitterSettings as {headWidthScale?: number}).headWidthScale = v;
1159
+ (s.rendererEmitterSettings as TrailSettings).lifetime = v > 0 ? v : undefined;
1004
1160
  })
1005
1161
  }
1006
1162
  />
1007
1163
  </Row>
1164
+ <Row label="Swept trail">
1165
+ <CheckboxField
1166
+ value={!!(system.rendererEmitterSettings as TrailSettings).sideAxis}
1167
+ onChange={(v) =>
1168
+ binding.apply((s) => {
1169
+ (s.rendererEmitterSettings as TrailSettings).sideAxis = v
1170
+ ? new Vector3(0, 1, 0)
1171
+ : undefined;
1172
+ })
1173
+ }
1174
+ />
1175
+ </Row>
1176
+ {(system.rendererEmitterSettings as TrailSettings).sideAxis && (
1177
+ <Row label="Side axis">
1178
+ <AxisField
1179
+ value={(system.rendererEmitterSettings as TrailSettings).sideAxis!}
1180
+ onChange={(axis) =>
1181
+ binding.apply((s) => {
1182
+ // The axis's length scales the width: keep it a unit vector.
1183
+ (s.rendererEmitterSettings as TrailSettings).sideAxis =
1184
+ axis.length() > 0 ? axis.normalize() : axis;
1185
+ })
1186
+ }
1187
+ />
1188
+ </Row>
1189
+ )}
1008
1190
  </>
1009
1191
  )}
1010
1192
  {system.renderMode === RenderMode.StretchedBillBoard && (
@@ -1033,26 +1215,7 @@ export function RendererModule({
1033
1215
  </Row>
1034
1216
  </>
1035
1217
  )}
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>
1218
+ <SubHeader title="Material" hint="Shader and blending settings applied to the particle sprite or mesh." />
1056
1219
  {materialLabel ? (
1057
1220
  <Row label="Source">
1058
1221
  <div style={{fontSize: 12, color: theme.textDim, lineHeight: 1.45}}>{materialLabel}</div>
@@ -1077,93 +1240,41 @@ export function RendererModule({
1077
1240
  />
1078
1241
  </Row>
1079
1242
  <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
- }}
1243
+ <TextureSelect
1244
+ value={system.texture}
1245
+ textureOptions={textureOptions}
1246
+ loadTexture={loadTexture}
1247
+ onChange={(texture) => patchMaterial({texture})}
1248
+ preview
1135
1249
  />
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
1250
  </Row>
1251
+ <TextureUVField
1252
+ labelPrefix="Map "
1253
+ value={system.mapUV ?? normalizeTextureUV(undefined)}
1254
+ onChange={(uv) => binding.apply((s) => (s.mapUV = uv))}
1255
+ />
1256
+ {!isMesh && (
1257
+ <Row label="Flip U/V">
1258
+ <div style={{display: 'flex', gap: 6}}>
1259
+ {(['x', 'y'] as const).map((axis) => (
1260
+ <NumberField
1261
+ key={axis}
1262
+ value={system.flip?.[axis] ?? 0}
1263
+ min={0}
1264
+ max={1}
1265
+ step={0.1}
1266
+ onChange={(v) =>
1267
+ binding.apply((s) => {
1268
+ const next = s.flip?.clone() ?? new Vector2(0, 0);
1269
+ next[axis] = Math.min(1, Math.max(0, v));
1270
+ s.flip = next.x === 0 && next.y === 0 ? undefined : next;
1271
+ })
1272
+ }
1273
+ />
1274
+ ))}
1275
+ </div>
1276
+ </Row>
1277
+ )}
1167
1278
  <Row label="Alpha test">
1168
1279
  <NumberField
1169
1280
  value={system.getRendererSettings().materialAlphaTest ?? 0}
@@ -1184,6 +1295,134 @@ export function RendererModule({
1184
1295
  onChange={(v) => patchMaterial({depthTest: v})}
1185
1296
  />
1186
1297
  </Row>
1298
+ <SubHeader
1299
+ title="Gradient map"
1300
+ hint="Recolours the map through a colour ramp: one channel of the map, or its luminance, picks a colour along the ramp."
1301
+ />
1302
+ <Row label="Ramp">
1303
+ <TextureSelect
1304
+ value={gradientMap?.ramp}
1305
+ textureOptions={textureOptions}
1306
+ loadTexture={loadTexture}
1307
+ promptTitle="Ramp texture URL"
1308
+ onChange={(ramp) =>
1309
+ binding.apply((s) => (s.gradientMap = ramp ? {...s.gradientMap, ramp} : undefined))
1310
+ }
1311
+ />
1312
+ </Row>
1313
+ {gradientMap && (
1314
+ <Row label="Ramp channel">
1315
+ <SelectField<GradientMapChannel>
1316
+ value={gradientMap.channel}
1317
+ options={[
1318
+ {value: 'r', label: 'Red'},
1319
+ {value: 'g', label: 'Green'},
1320
+ {value: 'b', label: 'Blue'},
1321
+ {value: 'a', label: 'Alpha (ramp alpha replaces)'},
1322
+ {value: 'luminance', label: 'Luminance'},
1323
+ ]}
1324
+ onChange={(channel) => binding.apply((s) => (s.gradientMap = {...s.gradientMap, channel}))}
1325
+ />
1326
+ </Row>
1327
+ )}
1328
+ <SubHeader
1329
+ title="Erosion"
1330
+ 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."
1331
+ />
1332
+ <Row label="Erosion">
1333
+ <CheckboxField
1334
+ value={!!erosion}
1335
+ onChange={(v) => binding.apply((s) => (s.erosion = v ? {} : undefined))}
1336
+ />
1337
+ </Row>
1338
+ {erosion && (
1339
+ <>
1340
+ <Row label="Erosion mask">
1341
+ <TextureSelect
1342
+ value={erosion.mask}
1343
+ textureOptions={textureOptions}
1344
+ loadTexture={loadTexture}
1345
+ noneLabel="(map alpha)"
1346
+ promptTitle="Erosion mask URL"
1347
+ onChange={(mask) => patchErosion({mask})}
1348
+ />
1349
+ </Row>
1350
+ {erosion.mask && (
1351
+ <TextureUVField
1352
+ labelPrefix="Mask "
1353
+ value={erosion.maskUV}
1354
+ onChange={(maskUV) => patchErosion({maskUV})}
1355
+ />
1356
+ )}
1357
+ <Row label="Softness">
1358
+ <NumberField
1359
+ value={erosion.softness}
1360
+ min={0.0001}
1361
+ step={0.01}
1362
+ onChange={(softness) => patchErosion({softness})}
1363
+ />
1364
+ </Row>
1365
+ <Row label="Edge width">
1366
+ <NumberField
1367
+ value={erosion.edgeWidth}
1368
+ min={0}
1369
+ step={0.01}
1370
+ onChange={(edgeWidth) => patchErosion({edgeWidth})}
1371
+ />
1372
+ </Row>
1373
+ {(erosion.edgeWidth > 0 || erosion.edgeColor.some((c) => c !== 1)) && (
1374
+ <Row label="Edge color">
1375
+ <div style={{display: 'flex', gap: 6}}>
1376
+ {[0, 1, 2].map((channel) => (
1377
+ <NumberField
1378
+ key={channel}
1379
+ value={erosion.edgeColor[channel]}
1380
+ min={0}
1381
+ step={0.05}
1382
+ onChange={(v) => {
1383
+ const edgeColor: [number, number, number] = [...erosion.edgeColor];
1384
+ edgeColor[channel] = v;
1385
+ patchErosion({edgeColor});
1386
+ }}
1387
+ />
1388
+ ))}
1389
+ </div>
1390
+ </Row>
1391
+ )}
1392
+ </>
1393
+ )}
1394
+ <SubHeader
1395
+ title="UV distortion"
1396
+ hint="A scrolling noise texture pushes the map's UV about, for heat shimmer and licking, breathing shapes."
1397
+ />
1398
+ <Row label="Noise texture">
1399
+ <TextureSelect
1400
+ value={distortion?.map}
1401
+ textureOptions={textureOptions}
1402
+ loadTexture={loadTexture}
1403
+ promptTitle="Distortion noise URL"
1404
+ onChange={(map) =>
1405
+ binding.apply((s) => (s.uvDistortion = map ? {...s.uvDistortion, map} : undefined))
1406
+ }
1407
+ />
1408
+ </Row>
1409
+ {distortion && (
1410
+ <>
1411
+ <Row label="Distortion strength">
1412
+ <NumberField
1413
+ value={distortion.strength}
1414
+ min={0}
1415
+ step={0.01}
1416
+ onChange={(strength) => patchDistortion({strength})}
1417
+ />
1418
+ </Row>
1419
+ <TextureUVField
1420
+ labelPrefix="Noise "
1421
+ value={distortion.mapUV}
1422
+ onChange={(mapUV) => patchDistortion({mapUV})}
1423
+ />
1424
+ </>
1425
+ )}
1187
1426
  {system.renderMode === RenderMode.Mesh &&
1188
1427
  (() => {
1189
1428
  const preset = detectGeometryPreset(system.instancingGeometry);
@@ -1193,10 +1432,12 @@ export function RendererModule({
1193
1432
  const applyGeometry = (g: GeometryData) =>
1194
1433
  binding.apply((s) => {
1195
1434
  const settings = s.getRendererSettings();
1435
+ // Positions first: the setter keeps or drops the other sets by vertex count.
1436
+ s.instancingGeometry = new Float32Array(g.positions);
1196
1437
  settings.instancingIndices = (g.indices ? new Uint32Array(g.indices) : undefined) as never;
1197
1438
  settings.instancingUVs = (g.uvs ? new Float32Array(g.uvs) : undefined) as never;
1439
+ settings.instancingUVs2 = (g.uvs2 ? new Float32Array(g.uvs2) : undefined) as never;
1198
1440
  settings.instancingNormals = (g.normals ? new Float32Array(g.normals) : undefined) as never;
1199
- s.instancingGeometry = new Float32Array(g.positions);
1200
1441
  });
1201
1442
  return (
1202
1443
  <Row label="Geometry">
@@ -1233,6 +1474,7 @@ export function RendererModule({
1233
1474
  }
1234
1475
  binding.apply((s) => {
1235
1476
  const settings = s.getRendererSettings();
1477
+ settings.instancingUVs2 = undefined;
1236
1478
  if (next === 'cube') {
1237
1479
  settings.instancingIndices = MESH_CUBE_INDICES;
1238
1480
  settings.instancingUVs = undefined;
@@ -1252,7 +1494,9 @@ export function RendererModule({
1252
1494
  />
1253
1495
  {preset === 'custom' && (
1254
1496
  <span style={{fontSize: 10.5, color: '#9eb9ff'}}>
1255
- {vertexCount} verts / {triangleCount} tris — picking a preset replaces this mesh
1497
+ {vertexCount} verts / {triangleCount} tris
1498
+ {system.getRendererSettings().instancingUVs2 ? ' / 2 UV sets' : ''} — picking a
1499
+ preset replaces this mesh
1256
1500
  </span>
1257
1501
  )}
1258
1502
  </div>
@@ -1298,6 +1542,7 @@ export function RendererModule({
1298
1542
  options={[
1299
1543
  {value: 'add', label: 'Add (glow)'},
1300
1544
  {value: 'mix', label: 'Mix (outline)'},
1545
+ {value: 'fade', label: 'Fade (faces fade, edges stay)'},
1301
1546
  ]}
1302
1547
  onChange={(mode) => patchRim({mode})}
1303
1548
  />