babylon.quarks-editor 0.23.0 → 0.24.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.
@@ -1,9 +1,12 @@
1
1
  import {Texture} from '@babylonjs/core/Materials/Textures/texture';
2
2
  import {Mesh} from '@babylonjs/core/Meshes/mesh';
3
3
  import {PlusIcon, XMarkIcon} from '@heroicons/react/24/solid';
4
- import type {Behavior, MeshLighting, MeshRim, ParticleSystem} from 'babylon.quarks';
4
+ import type {Behavior, CullingMode, MeshLighting, MeshRim, ParticleSystem, SortMode} from 'babylon.quarks';
5
5
  import {
6
+ AxisAngleGenerator,
7
+ BoxEmitFrom,
6
8
  ColorOverLife,
9
+ ColorRange,
7
10
  ConstantColor,
8
11
  ConstantValue,
9
12
  EulerGenerator,
@@ -14,6 +17,7 @@ import {
14
17
  RandomQuatGenerator,
15
18
  RenderMode,
16
19
  SizeOverLife,
20
+ Vector3,
17
21
  Vector3Function,
18
22
  Vector4,
19
23
  } from 'babylon.quarks';
@@ -97,6 +101,8 @@ const PARAM_LABELS: {[key: string]: string} = {
97
101
  height: 'Height',
98
102
  column: 'Columns',
99
103
  row: 'Rows',
104
+ depth: 'Depth',
105
+ speed: 'Speed (lengths/s)',
100
106
  };
101
107
 
102
108
  function ColorInput(props: {value: Vector4; onChange: (next: Vector4) => void}) {
@@ -133,28 +139,35 @@ function ColorInput(props: {value: Vector4; onChange: (next: Vector4) => void})
133
139
  export function MainModule({binding}: ModuleProps) {
134
140
  const system = binding.system;
135
141
  const startColor = system.startColor;
136
- const colorMode = startColor instanceof RandomColor ? 'random' : 'constant';
137
- const colorA =
138
- startColor instanceof RandomColor
139
- ? (startColor as never as {a: Vector4}).a
140
- : startColor instanceof ConstantColor
141
- ? startColor.color
142
- : new Vector4(1, 1, 1, 1);
143
- const colorB =
144
- startColor instanceof RandomColor ? (startColor as never as {b: Vector4}).b : new Vector4(1, 0.5, 0.2, 1);
142
+ const colorMode =
143
+ 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
147
+ : startColor instanceof ConstantColor
148
+ ? startColor.color
149
+ : 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);
151
+ /** The two-colour generator of the current mode. */
152
+ const makeTwoColors = (a: Vector4, b: Vector4) =>
153
+ colorMode === 'range' ? new ColorRange(a, b) : new RandomColor(a, b);
145
154
  const rotation = system.startRotation;
146
155
  const isEuler = rotation instanceof EulerGenerator;
156
+ const isAxisAngle = rotation instanceof AxisAngleGenerator;
147
157
  const rotationMode =
148
158
  rotation instanceof RandomQuatGenerator
149
159
  ? '3d'
150
160
  : isEuler
151
161
  ? 'euler'
152
- : readScalar(rotation as never).mode === 'random'
153
- ? 'random'
154
- : 'angle';
162
+ : isAxisAngle
163
+ ? 'axisAngle'
164
+ : readScalar(rotation as never).mode === 'random'
165
+ ? 'random'
166
+ : 'angle';
155
167
  const rotationState = readScalar(
156
- rotation instanceof RandomQuatGenerator || isEuler ? undefined : (rotation as never)
168
+ rotation instanceof RandomQuatGenerator || isEuler || isAxisAngle ? undefined : (rotation as never)
157
169
  );
170
+ const axisAngle = isAxisAngle ? (rotation as AxisAngleGenerator) : undefined;
158
171
  const eulerAngles = {
159
172
  x: isEuler ? readScalar((rotation as EulerGenerator).angleX as never).value : 0,
160
173
  y: isEuler ? readScalar((rotation as EulerGenerator).angleY as never).value : 0,
@@ -195,6 +208,39 @@ export function MainModule({binding}: ModuleProps) {
195
208
  <Row label="Prewarm">
196
209
  <CheckboxField value={system.prewarm} onChange={(v) => binding.apply((s) => (s.prewarm = v))} />
197
210
  </Row>
211
+ <Row label="Playback speed">
212
+ <NumberField
213
+ value={system.playbackSpeed}
214
+ step={0.1}
215
+ min={0}
216
+ onChange={(v) => binding.apply((s) => (s.playbackSpeed = Math.max(0, v)))}
217
+ />
218
+ </Row>
219
+ <Row label="Fixed seed">
220
+ <CheckboxField
221
+ value={system.seed !== undefined}
222
+ onChange={(v) => binding.apply((s) => (s.seed = v ? (s.seed ?? 1) : undefined))}
223
+ />
224
+ {system.seed !== undefined && (
225
+ <NumberField
226
+ value={system.seed}
227
+ step={1}
228
+ min={0}
229
+ onChange={(v) => binding.apply((s) => (s.seed = Math.max(0, Math.floor(v))))}
230
+ />
231
+ )}
232
+ </Row>
233
+ <Row label="Off screen">
234
+ <SelectField
235
+ value={system.cullingMode}
236
+ options={[
237
+ {value: 'always', label: 'Always simulate'},
238
+ {value: 'pause', label: 'Pause'},
239
+ {value: 'pauseAndCatchUp', label: 'Pause and catch up'},
240
+ ]}
241
+ onChange={(mode) => binding.apply((s) => (s.cullingMode = mode as CullingMode))}
242
+ />
243
+ </Row>
198
244
  <ValueField
199
245
  label="Start delay"
200
246
  generator={system.startDelay}
@@ -280,6 +326,7 @@ export function MainModule({binding}: ModuleProps) {
280
326
  options={[
281
327
  {value: 'constant', label: 'Constant'},
282
328
  {value: 'random', label: 'Random between two'},
329
+ {value: 'range', label: 'Blend between two'},
283
330
  {value: 'gradient', label: 'Gradient'},
284
331
  {value: 'gradient2', label: 'Random between gradients'},
285
332
  ]}
@@ -287,6 +334,8 @@ export function MainModule({binding}: ModuleProps) {
287
334
  binding.apply((s) => {
288
335
  if (mode === 'random') {
289
336
  s.startColor = new RandomColor(colorA, colorB);
337
+ } else if (mode === 'range') {
338
+ s.startColor = new ColorRange(colorA, colorB);
290
339
  } else if (mode === 'gradient') {
291
340
  s.startColor = buildGradient(DEFAULT_GRADIENT_STOPS);
292
341
  } else if (mode === 'gradient2') {
@@ -301,24 +350,23 @@ export function MainModule({binding}: ModuleProps) {
301
350
  }
302
351
  />
303
352
  </Row>
304
- {(colorGenMode === 'constant' || colorGenMode === 'random') && (
353
+ {(colorGenMode === 'constant' || twoColors) && (
305
354
  <Row label="">
306
355
  <ColorInput
307
356
  value={colorA}
308
357
  onChange={(next) =>
309
358
  binding.apply((s) => {
310
- s.startColor =
311
- colorGenMode === 'random' ? new RandomColor(next, colorB) : new ConstantColor(next);
359
+ s.startColor = twoColors ? makeTwoColors(next, colorB) : new ConstantColor(next);
312
360
  })
313
361
  }
314
362
  />
315
363
  </Row>
316
364
  )}
317
- {colorGenMode === 'random' && (
365
+ {twoColors && (
318
366
  <Row label="">
319
367
  <ColorInput
320
368
  value={colorB}
321
- onChange={(next) => binding.apply((s) => (s.startColor = new RandomColor(colorA, next)))}
369
+ onChange={(next) => binding.apply((s) => (s.startColor = makeTwoColors(colorA, next)))}
322
370
  />
323
371
  </Row>
324
372
  )}
@@ -359,6 +407,7 @@ export function MainModule({binding}: ModuleProps) {
359
407
  {value: 'angle', label: 'Angle (rad)'},
360
408
  {value: 'random', label: 'Random angle'},
361
409
  {value: 'euler', label: '3D (per axis)'},
410
+ {value: 'axisAngle', label: '3D (axis + angle)'},
362
411
  {value: '3d', label: 'Random 3D'},
363
412
  ]}
364
413
  onChange={(mode) =>
@@ -366,15 +415,20 @@ export function MainModule({binding}: ModuleProps) {
366
415
  s.startRotation =
367
416
  mode === '3d'
368
417
  ? new RandomQuatGenerator()
369
- : mode === 'euler'
370
- ? new EulerGenerator(
371
- new ConstantValue(eulerAngles.x),
372
- new ConstantValue(eulerAngles.y),
373
- new ConstantValue(eulerAngles.z)
418
+ : mode === 'axisAngle'
419
+ ? new AxisAngleGenerator(
420
+ new Vector3(0, 1, 0),
421
+ new ConstantValue(rotationState.value)
374
422
  )
375
- : mode === 'random'
376
- ? new IntervalValue(0, Math.PI * 2)
377
- : new ConstantValue(rotationState.value);
423
+ : mode === 'euler'
424
+ ? new EulerGenerator(
425
+ new ConstantValue(eulerAngles.x),
426
+ new ConstantValue(eulerAngles.y),
427
+ new ConstantValue(eulerAngles.z)
428
+ )
429
+ : mode === 'random'
430
+ ? new IntervalValue(0, Math.PI * 2)
431
+ : new ConstantValue(rotationState.value);
378
432
  })
379
433
  }
380
434
  />
@@ -388,6 +442,36 @@ export function MainModule({binding}: ModuleProps) {
388
442
  ))}
389
443
  </>
390
444
  )}
445
+ {axisAngle && (
446
+ <>
447
+ <Row label=" Axis">
448
+ <div style={{display: 'flex', gap: 6}}>
449
+ {(['x', 'y', 'z'] as const).map((axis) => (
450
+ <NumberField
451
+ key={axis}
452
+ value={axisAngle.axis[axis]}
453
+ step={0.1}
454
+ onChange={(v) =>
455
+ binding.apply((s) => {
456
+ const next = axisAngle.axis.clone();
457
+ next[axis] = v;
458
+ s.startRotation = new AxisAngleGenerator(next, axisAngle.angle);
459
+ })
460
+ }
461
+ />
462
+ ))}
463
+ </div>
464
+ </Row>
465
+ <ValueField
466
+ label=" Angle (rad)"
467
+ generator={axisAngle.angle}
468
+ curveMax={Math.PI * 2}
469
+ onChange={(g) =>
470
+ binding.apply((s) => (s.startRotation = new AxisAngleGenerator(axisAngle.axis.clone(), g)))
471
+ }
472
+ />
473
+ </>
474
+ )}
391
475
  {rotationMode === 'angle' && (
392
476
  <Row label="">
393
477
  <NumberField
@@ -541,7 +625,7 @@ export function EmissionModule({binding}: ModuleProps) {
541
625
  // Shapes whose constructor doesn't accept `mode`/`spread` at all — showing those controls
542
626
  // for them would edit a value the shape silently ignores, just like the old "default to
543
627
  // first texture" bug: a control that looks live but does nothing.
544
- const SHAPES_WITHOUT_MODE_SPREAD = new Set<ShapeType>(['point', 'grid', 'mesh_surface']);
628
+ const SHAPES_WITHOUT_MODE_SPREAD = new Set<ShapeType>(['point', 'grid', 'box', 'mesh_surface']);
545
629
 
546
630
  export function ShapeModule({binding}: ModuleProps) {
547
631
  const system = binding.system;
@@ -589,6 +673,21 @@ export function ShapeModule({binding}: ModuleProps) {
589
673
  />
590
674
  </Row>
591
675
  )}
676
+ {type === 'box' && (
677
+ <Row label="Emit from">
678
+ <SelectField
679
+ value={params.emitFrom}
680
+ options={[
681
+ {value: BoxEmitFrom.Volume, label: 'Volume'},
682
+ {value: BoxEmitFrom.Shell, label: 'Shell (faces)'},
683
+ {value: BoxEmitFrom.Edge, label: 'Edges'},
684
+ ]}
685
+ onChange={(emitFrom) =>
686
+ binding.apply((s) => (s.emitterShape = createShape(type, {...params, emitFrom})))
687
+ }
688
+ />
689
+ </Row>
690
+ )}
592
691
  {!SHAPES_WITHOUT_MODE_SPREAD.has(type) && (
593
692
  <>
594
693
  <Row label="Emit mode">
@@ -617,6 +716,12 @@ export function ShapeModule({binding}: ModuleProps) {
617
716
  </Row>
618
717
  </>
619
718
  )}
719
+ <Row label="Align to direction">
720
+ <CheckboxField
721
+ value={binding.system.alignToDirection}
722
+ onChange={(v) => binding.apply((s) => (s.alignToDirection = v))}
723
+ />
724
+ </Row>
620
725
  </ModuleSection>
621
726
  );
622
727
  }
@@ -868,6 +973,37 @@ export function RendererModule({
868
973
  }
869
974
  />
870
975
  </Row>
976
+ {(
977
+ [
978
+ ['recordWorldSpace', 'Record in world space'],
979
+ ['uvFromHead', 'UV from head'],
980
+ ['colorFromParticle', 'Color from particle'],
981
+ ['dieWithParticle', 'Die with particle'],
982
+ ] as const
983
+ ).map(([key, label]) => (
984
+ <Row key={key} label={label}>
985
+ <CheckboxField
986
+ value={!!(system.rendererEmitterSettings as Record<string, unknown>)[key]}
987
+ onChange={(v) =>
988
+ binding.apply((s) => {
989
+ (s.rendererEmitterSettings as Record<string, unknown>)[key] = v;
990
+ })
991
+ }
992
+ />
993
+ </Row>
994
+ ))}
995
+ <Row label="Head width scale">
996
+ <NumberField
997
+ value={(system.rendererEmitterSettings as {headWidthScale?: number}).headWidthScale ?? 1}
998
+ min={0}
999
+ step={0.1}
1000
+ onChange={(v) =>
1001
+ binding.apply((s) => {
1002
+ (s.rendererEmitterSettings as {headWidthScale?: number}).headWidthScale = v;
1003
+ })
1004
+ }
1005
+ />
1006
+ </Row>
871
1007
  </>
872
1008
  )}
873
1009
  {system.renderMode === RenderMode.StretchedBillBoard && (
@@ -1204,6 +1340,124 @@ export function RendererModule({
1204
1340
  onChange={(v) => binding.apply((s) => (s.renderOrder = Math.round(v)))}
1205
1341
  />
1206
1342
  </Row>
1343
+ <Row label="Rendering group">
1344
+ <NumberField
1345
+ value={system.renderingGroupId}
1346
+ step={1}
1347
+ min={0}
1348
+ onChange={(v) => binding.apply((s) => (s.renderingGroupId = Math.max(0, Math.round(v))))}
1349
+ />
1350
+ </Row>
1351
+ <Row label="Layer mask">
1352
+ <NumberField
1353
+ value={system.layers.mask}
1354
+ min={0}
1355
+ step={1}
1356
+ onChange={(v) => binding.apply((s) => (s.layers.mask = Math.max(0, Math.floor(v))))}
1357
+ />
1358
+ </Row>
1359
+ <Row label="Max particles">
1360
+ <CheckboxField
1361
+ value={system.maxParticles !== undefined}
1362
+ onChange={(v) => binding.apply((s) => (s.maxParticles = v ? 1000 : undefined))}
1363
+ />
1364
+ {system.maxParticles !== undefined && (
1365
+ <NumberField
1366
+ value={system.maxParticles}
1367
+ min={1}
1368
+ step={10}
1369
+ onChange={(v) => binding.apply((s) => (s.maxParticles = Math.max(1, Math.floor(v))))}
1370
+ />
1371
+ )}
1372
+ </Row>
1373
+ {system.renderMode !== RenderMode.Mesh && system.renderMode !== RenderMode.Trail && (
1374
+ <Row label="Pivot">
1375
+ <div style={{display: 'flex', gap: 6}}>
1376
+ {(['x', 'y', 'z'] as const).map((axis) => (
1377
+ <NumberField
1378
+ key={axis}
1379
+ value={system.pivot?.[axis] ?? 0}
1380
+ step={0.05}
1381
+ onChange={(v) =>
1382
+ binding.apply((s) => {
1383
+ const next = (s.pivot ?? new Vector3()).clone();
1384
+ next[axis] = v;
1385
+ s.pivot = next;
1386
+ })
1387
+ }
1388
+ />
1389
+ ))}
1390
+ </div>
1391
+ </Row>
1392
+ )}
1393
+ {system.renderMode === RenderMode.Mesh && (
1394
+ <>
1395
+ <Row label="Env map atlas">
1396
+ <SelectField
1397
+ value={(system.getRendererSettings().reflectionAtlas as {url?: string} | null)?.url ?? ''}
1398
+ options={[
1399
+ {value: '', label: 'None'},
1400
+ ...(textureOptions ?? []).map((option) => ({value: option.url, label: option.label})),
1401
+ ]}
1402
+ onChange={(url) => patchMaterial({reflectionAtlas: url ? loadTexture(url) : null})}
1403
+ />
1404
+ </Row>
1405
+ <Row label="Reflection level">
1406
+ <NumberField
1407
+ value={system.getRendererSettings().reflectionLevel ?? 1}
1408
+ min={0}
1409
+ step={0.1}
1410
+ onChange={(v) => patchMaterial({reflectionLevel: Math.max(0, v)})}
1411
+ />
1412
+ </Row>
1413
+ </>
1414
+ )}
1415
+ <Row label="Sort mode">
1416
+ <SelectField
1417
+ value={system.sortMode}
1418
+ options={[
1419
+ {value: 'none', label: 'None'},
1420
+ {value: 'distance', label: 'By distance'},
1421
+ {value: 'oldestInFront', label: 'Oldest in front'},
1422
+ {value: 'youngestInFront', label: 'Youngest in front'},
1423
+ ]}
1424
+ onChange={(mode) => binding.apply((s) => (s.sortMode = mode as SortMode))}
1425
+ />
1426
+ </Row>
1427
+ <Row label="Limit screen size">
1428
+ <CheckboxField
1429
+ value={system.minParticleSize > 0 || system.maxParticleSize !== Infinity}
1430
+ onChange={(v) =>
1431
+ binding.apply((s) => {
1432
+ s.minParticleSize = 0;
1433
+ s.maxParticleSize = v ? 0.5 : Infinity;
1434
+ })
1435
+ }
1436
+ />
1437
+ </Row>
1438
+ {(system.minParticleSize > 0 || system.maxParticleSize !== Infinity) && (
1439
+ <>
1440
+ <Row label="Min size (viewport)">
1441
+ <NumberField
1442
+ value={system.minParticleSize}
1443
+ step={0.01}
1444
+ min={0}
1445
+ onChange={(v) => binding.apply((s) => (s.minParticleSize = Math.max(0, v)))}
1446
+ />
1447
+ </Row>
1448
+ <Row label="Max size (viewport)">
1449
+ <NumberField
1450
+ value={system.maxParticleSize === Infinity ? 0 : system.maxParticleSize}
1451
+ step={0.05}
1452
+ min={0}
1453
+ onChange={(v) => binding.apply((s) => (s.maxParticleSize = v > 0 ? v : Infinity))}
1454
+ />
1455
+ </Row>
1456
+ </>
1457
+ )}
1458
+ <Row label="Apply fog">
1459
+ <CheckboxField value={system.applyFog} onChange={(v) => binding.apply((s) => (s.applyFog = v))} />
1460
+ </Row>
1207
1461
  <Row label="Soft particles">
1208
1462
  <CheckboxField
1209
1463
  value={system.softParticles}