phoria2 1.0.0 → 1.1.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 (46) hide show
  1. package/README.md +6 -6
  2. package/dist/bloom.js +4 -4
  3. package/dist/bloom.js.map +1 -1
  4. package/dist/charts.js +377 -377
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.js +1052 -558
  7. package/dist/core.js.map +1 -1
  8. package/dist/edit.js +48 -48
  9. package/dist/edit.js.map +1 -1
  10. package/dist/explain.js +1 -1
  11. package/dist/gltf.js +207 -162
  12. package/dist/gltf.js.map +1 -1
  13. package/dist/motion.js +27 -27
  14. package/dist/motion.js.map +1 -1
  15. package/dist/particles.js +50 -50
  16. package/dist/particles.js.map +1 -1
  17. package/dist/passes.js +199 -97
  18. package/dist/passes.js.map +1 -1
  19. package/dist/phoria2.js +1 -1
  20. package/dist/picking.js +16 -21
  21. package/dist/picking.js.map +1 -1
  22. package/dist/snapshot.js +1 -1
  23. package/dist/tooltip.js +3 -3
  24. package/dist/tooltip.js.map +1 -1
  25. package/dist/types/View.d.ts +5 -0
  26. package/dist/types/geometry/Geometry.d.ts +6 -0
  27. package/dist/types/material/Material.d.ts +125 -0
  28. package/dist/types/material/Texture.d.ts +37 -5
  29. package/dist/types/webgl/WebGLRenderer.d.ts +6 -0
  30. package/dist/types/webgl/passes.d.ts +37 -13
  31. package/dist/types/webgl/resources.d.ts +24 -2
  32. package/dist/types/webgl/shaders.d.ts +4 -2
  33. package/llms-full.txt +96 -7
  34. package/package.json +1 -1
  35. package/schema/scene.schema.json +127 -0
  36. package/src/View.js +4 -2
  37. package/src/geometry/Geometry.js +8 -1
  38. package/src/gltf/load.js +50 -6
  39. package/src/material/Material.js +104 -19
  40. package/src/material/Texture.js +116 -11
  41. package/src/watch.js +19 -0
  42. package/src/webgl/WebGLRenderer.js +51 -12
  43. package/src/webgl/passes.js +190 -32
  44. package/src/webgl/picking.js +0 -1
  45. package/src/webgl/resources.js +89 -8
  46. package/src/webgl/shaders.js +350 -48
package/llms-full.txt CHANGED
@@ -15,8 +15,8 @@ editors that drag, snap and undo. Picking, tooltips, hover highlights, HTML labe
15
15
  image-based light are built in, and a view draws only when something changes, so idle pages cost
16
16
  nothing. A dashboard of many small views just works - each tile its own `new View({ canvas })`,
17
17
  with no limit on WebGL contexts to manage (after the first four, views share one). Apps come out short: typically a few dozen lines. For games, custom shaders or
18
- post-processing pipelines, three.js is the better fit: phoria2 has no shader hook (no fresnel or
19
- custom effects), no clearcoat, and no shadow-softness setting.
18
+ post-processing pipelines, three.js is the better fit: phoria2 has no shader hook (no custom
19
+ effects), no refraction, and no shadow-softness setting.
20
20
 
21
21
  ## Install
22
22
 
@@ -285,8 +285,9 @@ extrude({
285
285
  })
286
286
  ```
287
287
 
288
- Your own: `new Geometry({ positions, normals?, uvs?, colors?, indices? })` - flat arrays, xyz per
289
- vertex; `colors` are rgb 0-1 per vertex (use `material: { vertexColors: true }`).
288
+ Your own: `new Geometry({ positions, normals?, uvs?, uvs2?, colors?, indices? })` - flat arrays, xyz per
289
+ vertex; `colors` are rgb 0-1 per vertex (use `material: { vertexColors: true }`); `uvs2` is a second UV
290
+ set, for baked occlusion and light maps (below).
290
291
 
291
292
  ## Materials
292
293
 
@@ -296,7 +297,14 @@ One material type, given as a plain object:
296
297
  { color: 'white' } // lit, physically based
297
298
  { color: '#c33', roughness: 0.2, metalness: 1 } // metal
298
299
  { color: 'orange', unlit: true } // flat colour, ignores lights
299
- { color: '#8cf', opacity: 0.4 } // transparent
300
+ { color: '#8cf', opacity: 0.4 } // transparent: fades the whole surface
301
+ { transmission: 1, roughness: 0.05 } // clear glass
302
+ { color: '#a3121f', metalness: 0.6, clearcoat: 1 } // car paint: a glossy coat over it
303
+ { color: '#1f2d6e', roughness: 0.9, sheen: 1 } // velvet: cloth's bright rim
304
+ { color: '#ccc', metalness: 1, anisotropy: 0.8 } // brushed metal
305
+ { color: '#f2e4c4', translucency: 0.8 } // paper: lit from behind too
306
+ { matcap: 'clay.png' } // the look from a picture of a lit sphere
307
+ { color: '#e4572e', toon: 3 } // cartoon: three flat tones
300
308
  { emissive: '#ff0', emissiveIntensity: 3 } // glows
301
309
  { map: 'wood.jpg', normalMap: 'wood_n.png' } // textures from urls
302
310
  ```
@@ -313,7 +321,8 @@ Properties: `color`, `opacity`, `roughness` (0.5), `metalness` (0), `emissive`,
313
321
  (`'normal'`, `'additive'` - adds light, for glows and sparks - or `'multiply'` - darkens what is behind, as a colour
314
322
  filter does: cyan over magenta is blue), `alphaTest` (cut-outs: drop pixels with less alpha - leaves, fences),
315
323
  `map`, `normalMap`/`normalScale`, `bumpMap`/`bumpScale`, `roughnessMap`, `metalnessMap`, `aoMap`,
316
- `emissiveMap`; `size`/`sizeAttenuation` for Points, `width` for Lines. Change later through
324
+ `emissiveMap`, `lightMap`/`lightMapIntensity`, `environmentIntensity` (1), `transmission`, `clearcoat`/`clearcoatRoughness`, `sheen`/`sheenColor`/`sheenRoughness`, `ior`/
325
+ `specularIntensity`/`specularColor`, `anisotropy`/`anisotropyRotation`, `translucency`/`translucencyColor`, `matcap`, `toon` (below); `size`/`sizeAttenuation` for Points, `width` for Lines. Change later through
317
326
  `mesh.material.color.set('#0f0')`, `mesh.material.opacity = 0.5`, or a whole new one: `mesh.material = { color: 'red' }`
318
327
  (options become a Material, as in the constructor; `mesh.geometry = box(2)` likewise).
319
328
 
@@ -324,6 +333,83 @@ into the distance. A cut-out (`alphaTest`) casts the shadow of what is left of i
324
333
  data map (`normalMap`, `bumpMap`, `roughnessMap`, `metalnessMap`, `aoMap`) is read as data, not colour,
325
334
  unless its `colorSpace` was set on purpose - `normalMap: await loadTexture('n.png')` just works.
326
335
 
336
+ **Canvas and video textures**: a texture's `image` can be a canvas - draw on it, then call
337
+ `texture.needsUpdate()` - or a `<video>`, whose frames upload by themselves while it plays, with
338
+ nothing to call. `new Texture({ url: 'promo.mp4' })` (or .webm, .ogv, .mov, .m4v) makes the video
339
+ for you, muted and looping so browsers let it start by itself; `texture.image` is that `<video>` -
340
+ pause, seek or unmute it as any other. A browser that blocks autoplay leaves it on its first frame
341
+ until `texture.image.play()` from a click. Video textures make no mipmaps (they would be remade
342
+ every frame) unless `mipmaps: true`. Put one on an unlit plane for a screen:
343
+ `{ map: new Texture({ url: 'promo.mp4', wrap: 'clamp' }), unlit: true }`.
344
+
345
+ **Placing a texture**: `repeat` ([u, v] times across), `offset`, and `rotation` (radians, about
346
+ `center`: [0, 0], the corner, by default - [0.5, 0.5] for the middle) - applied in that order, as glTF's
347
+ `KHR_texture_transform` does, and each map keeps its own: a fabric's colour can repeat 7 times while
348
+ its normal map repeats twice. Animate them to scroll or spin a pattern.
349
+
350
+ **Baked occlusion and light maps**: occlusion and lighting baked in Blender, Unreal or Unity need
351
+ every surface on its own patch of texture, so they come on a second UV set while the first tiles
352
+ colour and detail. Give the geometry `uvs2` and the texture `uvSet: 1` - glTF models do both
353
+ for you (`TEXCOORD_1`, `texCoord: 1`). Read through the first set, baked occlusion lands in the
354
+ wrong places as dark smudges. `lightMap` is light baked into a texture, added as light from all
355
+ round (`lightMapIntensity`, 1) - the soft, cheap lighting of architectural scenes.
356
+
357
+ **Environment strength per material**: `environmentIntensity` (1) scales how much of the
358
+ scene's environment - its light and its reflections - one material takes: lower for a matte floor in
359
+ a shiny showroom, higher to make one product catch the light, without changing the scene's.
360
+
361
+ **Glass**: `transmission` (0-1) lets light through as glass does. Unlike `opacity`, which fades
362
+ everything - reflections too, so faded things look washed out, as dimmed chart bars should - glass
363
+ keeps its reflections whole, and shows less of what is behind it towards its edges, where it
364
+ reflects nearly everything. Its colour shows in proportion to `1 - transmission`: `transmission: 0.6`
365
+ with a colour is tinted glass. Keep `roughness` low for clear glass, higher for frosted. There is
366
+ no refraction - what is behind isn't bent. It shines with an `environment` to reflect
367
+ (`'studio'`, or better an `.hdr`).
368
+
369
+ **Clearcoat**: `clearcoat` (0-1) puts a clear glossy layer over the surface - car paint, lacquered
370
+ wood, varnish - with its own sharp reflection over the base's softer one; `clearcoatRoughness` (0,
371
+ a mirror) makes it satin. Normal and bump maps show under a smooth coat.
372
+
373
+ **Sheen**: `sheen` (0-1) is the soft bright rim of cloth at grazing angles - velvet, felt,
374
+ upholstery - which plain roughness can't give: without it fabric looks like matte plastic.
375
+ `sheenColor` (white) tints it, often lighter than the cloth or another colour entirely;
376
+ `sheenRoughness` (0.5) runs from a thin bright rim (0) to a broad soft glow (1).
377
+
378
+ **Reflectance**: `ior` (1.5) sets how strongly a non-metal reflects - water 1.33, glass and plastic
379
+ 1.5, diamond 2.4 - and glass with `transmission` reflects more the higher it is.
380
+ `specularIntensity` (1) scales a non-metal's reflections, 0 for none (chalk, clay, unglazed
381
+ pottery); `specularColor` tints them face on. Metals take their reflections from `color`, so
382
+ these leave them alone.
383
+
384
+ **Brushed metal**: `anisotropy` (0-1) - the material's, not a texture's filtering - draws highlights and reflections out along the brushing -
385
+ brushed aluminium, steel appliances, the bottom of a pan. The brushing runs along the UVs' u (round
386
+ a sphere or cylinder, say; round the up axis on a mesh with no UVs); `anisotropyRotation`
387
+ (radians) turns it - `Math.PI / 2` runs it along v. Use it with `metalness: 1` and a middling
388
+ `roughness` (0.2-0.4).
389
+
390
+ **Translucency**: `translucency` (0-1) lets light through thin surfaces, so they are lit from
391
+ behind as well as in front - leaves against the sun, paper lanterns and lampshades with a light
392
+ inside, curtains. What passes through is taken from what is scattered back, so at 1 a surface is lit
393
+ only through it. `translucencyColor` (white) tints the light that comes through. It is light
394
+ through a thin sheet, not light scattering inside a solid: a lantern glows, marble stays marble.
395
+
396
+ **Matcap**: `matcap` is a picture of a lit sphere - clay, wax, chrome, a painted ball - that gives the
397
+ whole look: each point takes the colour from where a sphere facing the same way would show it. It is
398
+ the clay look of sculpting tools and CAD viewers, good for reading a model's shape, and costs one
399
+ texture lookup - no lights, shadows or environment, which it ignores. Its shine stays put as the camera
400
+ turns, since the light is in the picture, so it looks out of place beside lit things. `color` and
401
+ `map` multiply it, so one grey matcap serves any colour; normal and bump maps and `flatShading`
402
+ still shape it. Any matcap image works, or draw one on a canvas (`new Texture({ image: canvas })`).
403
+
404
+ **Toon**: `toon: 3` shades in that many flat tones (2 or more; 0, the default, is smooth), with one
405
+ hard-edged highlight - a cartoon. Lights and shadows work as usual; the shaded tone is the scene's
406
+ ambient and hemisphere light, so add some for a lighter shade. Metals keep their colour, with a
407
+ highlight of it. Pair it with `outline` on the nodes for inked edges.
408
+
409
+ glTF models bring glass, clearcoat, sheen, reflectance, brushed metal and translucency from `KHR_materials_transmission`, `KHR_materials_clearcoat`,
410
+ `KHR_materials_sheen`, `KHR_materials_ior`, `KHR_materials_specular`, `KHR_materials_anisotropy`
411
+ and `KHR_materials_diffuse_transmission` (by their factors; their textures aren't read).
412
+
327
413
  **Models without UVs** (scans, CAD exports) can still take maps: `triplanar: 40` projects them
328
414
  from the mesh's three axes instead of using UVs, repeating 40 times per unit of the mesh's own space.
329
415
  The maps stay on the mesh as it moves; normal maps follow the surface; a map's `repeat` and `offset` apply
@@ -458,7 +544,10 @@ chair.onDrag = (drag) => {
458
544
  ```
459
545
 
460
546
  - **Outline**: `outline: '#ffcc00'` or `{ color, width }` (pixels, up to 8; default 2) on a node draws
461
- a line round its silhouette, over everything - for what is selected. Set it any time; `null` removes it.
547
+ a line round its silhouette, over everything - for what is selected, or as an inked edge for a
548
+ drawn look. Set it any time; `null` removes it. Outlines are antialiased - soft edges and round
549
+ corners - as the canvas is; `outlineAntialias: false` on the View (or `view.renderer.outlineAntialias =
550
+ false`, any time) draws them in hard pixel steps instead.
462
551
  - `view.controls.cancel()` lets go of the orbit gesture in progress, for apps that take over a press.
463
552
 
464
553
  ## Charts: `phoria2/charts`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoria2",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Small, friendly 3D for the web - a WebGL 2 renderer with charts, labels, picking, particles and glTF",
5
5
  "keywords": [
6
6
  "3d",
@@ -703,6 +703,23 @@
703
703
  "minItems": 2,
704
704
  "maxItems": 2
705
705
  },
706
+ "rotation": {
707
+ "type": "number",
708
+ "description": "Turns the image on the surface, in radians, about center."
709
+ },
710
+ "center": {
711
+ "type": "array",
712
+ "description": "The point rotation turns about, in repeated UVs ([0, 0], the corner, by default).",
713
+ "items": {
714
+ "type": "number"
715
+ },
716
+ "minItems": 2,
717
+ "maxItems": 2
718
+ },
719
+ "uvSet": {
720
+ "enum": [0, 1],
721
+ "description": "Which UV set to read: 0 the first, 1 the second (uvs2) - baked occlusion and light maps usually come on the second."
722
+ },
706
723
  "filter": {
707
724
  "enum": ["linear", "nearest"]
708
725
  },
@@ -801,6 +818,28 @@
801
818
  }
802
819
  ]
803
820
  },
821
+ "lightMap": {
822
+ "description": "Light baked into a texture, added as light from all round - usually on the second UV set.",
823
+ "anyOf": [
824
+ {
825
+ "$ref": "#/$defs/texture"
826
+ },
827
+ {
828
+ "type": "null"
829
+ }
830
+ ]
831
+ },
832
+ "matcap": {
833
+ "description": "A picture of a lit sphere that gives the whole look - clay, wax, chrome - picked by which way the surface faces the camera; lights are ignored.",
834
+ "anyOf": [
835
+ {
836
+ "$ref": "#/$defs/texture"
837
+ },
838
+ {
839
+ "type": "null"
840
+ }
841
+ ]
842
+ },
804
843
  "unlit": {
805
844
  "type": "boolean",
806
845
  "description": "Ignore lights: draws exactly its colour."
@@ -873,6 +912,15 @@
873
912
  "minimum": 0,
874
913
  "maximum": 1
875
914
  },
915
+ "lightMapIntensity": {
916
+ "type": "number",
917
+ "minimum": 0
918
+ },
919
+ "environmentIntensity": {
920
+ "type": "number",
921
+ "description": "This material's share of the environment's light and reflections (1).",
922
+ "minimum": 0
923
+ },
876
924
  "triplanar": {
877
925
  "type": "number",
878
926
  "description": "Repeats per unit, projecting maps from three axes instead of using UVs; 0 uses the UVs.",
@@ -881,6 +929,78 @@
881
929
  "bumpScale": {
882
930
  "type": "number"
883
931
  },
932
+ "transmission": {
933
+ "type": "number",
934
+ "description": "How much light passes through, as through glass: reflections stay whole, and less of what is behind shows towards the edges. Unlike opacity, which fades the whole surface.",
935
+ "minimum": 0,
936
+ "maximum": 1
937
+ },
938
+ "clearcoat": {
939
+ "type": "number",
940
+ "description": "A clear glossy layer over the surface - car paint, lacquer, varnish.",
941
+ "minimum": 0,
942
+ "maximum": 1
943
+ },
944
+ "clearcoatRoughness": {
945
+ "type": "number",
946
+ "description": "How glossy the coat is, 0 a mirror.",
947
+ "minimum": 0,
948
+ "maximum": 1
949
+ },
950
+ "sheen": {
951
+ "type": "number",
952
+ "description": "The soft bright rim of cloth at grazing angles - velvet, felt, upholstery.",
953
+ "minimum": 0,
954
+ "maximum": 1
955
+ },
956
+ "sheenColor": {
957
+ "$ref": "#/$defs/color"
958
+ },
959
+ "sheenRoughness": {
960
+ "type": "number",
961
+ "description": "A thin bright rim at 0, a broad soft glow at 1.",
962
+ "minimum": 0,
963
+ "maximum": 1
964
+ },
965
+ "ior": {
966
+ "type": "number",
967
+ "description": "Index of refraction: how strongly a non-metal reflects - water 1.33, glass and plastic 1.5 (the default), diamond 2.4.",
968
+ "minimum": 1
969
+ },
970
+ "specularIntensity": {
971
+ "type": "number",
972
+ "description": "Scales a non-metal's reflections, 0 for none.",
973
+ "minimum": 0,
974
+ "maximum": 1
975
+ },
976
+ "specularColor": {
977
+ "$ref": "#/$defs/color"
978
+ },
979
+ "anisotropy": {
980
+ "type": "number",
981
+ "description": "Brushed metal: highlights drawn out along the brushing, which runs along the UVs' u.",
982
+ "minimum": 0,
983
+ "maximum": 1
984
+ },
985
+ "anisotropyRotation": {
986
+ "type": "number",
987
+ "description": "Turns the brushing from the UVs' u, in radians."
988
+ },
989
+ "translucency": {
990
+ "type": "number",
991
+ "description": "Light through thin surfaces, lighting them from behind - leaves, paper, lampshades.",
992
+ "minimum": 0,
993
+ "maximum": 1
994
+ },
995
+ "translucencyColor": {
996
+ "$ref": "#/$defs/color"
997
+ },
998
+ "toon": {
999
+ "type": "number",
1000
+ "description": "Cartoon shading in this many flat tones (2 or more), with a hard-edged highlight; 0 is smooth.",
1001
+ "minimum": 0,
1002
+ "maximum": 16
1003
+ },
884
1004
  "blending": {
885
1005
  "enum": ["normal", "additive", "multiply"]
886
1006
  }
@@ -1129,6 +1249,13 @@
1129
1249
  "type": "number"
1130
1250
  }
1131
1251
  },
1252
+ "uvs2": {
1253
+ "type": "array",
1254
+ "description": "A second UV set, for baked occlusion and light maps.",
1255
+ "items": {
1256
+ "type": "number"
1257
+ }
1258
+ },
1132
1259
  "colors": {
1133
1260
  "type": "array",
1134
1261
  "items": {
package/src/View.js CHANGED
@@ -53,6 +53,8 @@ import { checkOneOf, checkOptions, checkRange, describe, fail, warnOnce } from '
53
53
  * @property {(hit: Hit) => TooltipContent} [tooltip] what to show for the thing under the pointer - see tooltip()
54
54
  * @property {number} [pixelRatio] default min(devicePixelRatio, 2)
55
55
  * @property {boolean} [antialias] (default true)
56
+ * @property {boolean} [outlineAntialias] soft-edged outlines with round corners (default: as antialias); change it
57
+ * any time with view.renderer.outlineAntialias
56
58
  * @property {'auto' | 'own' | 'shared'} [context] which WebGL context draws it: 'own' gives the canvas one of
57
59
  * its own; 'shared' draws with the page's one shared context and copies the picture over, so a page can hold
58
60
  * any number of views (browsers allow about 16 live contexts). 'auto' (default): own for the first four
@@ -69,7 +71,7 @@ import { checkOneOf, checkOptions, checkRange, describe, fail, warnOnce } from '
69
71
  /** @typedef {(hit: Hit | null, event: PointerEvent | MouseEvent | null) => void} ViewHandler */
70
72
 
71
73
  const OPTIONS = ['canvas', 'scene', 'camera', 'orbit', 'background', 'renderMode', 'framing', 'highlight', 'tooltip', 'pixelRatio',
72
- 'antialias', 'context', 'toneMapping', 'exposure', 'bloom', 'autoLight', 'maxDelta']
74
+ 'antialias', 'outlineAntialias', 'context', 'toneMapping', 'exposure', 'bloom', 'autoLight', 'maxDelta']
73
75
  const FRAMING_OPTIONS = ['keep', 'aspect']
74
76
  const HINTS = {
75
77
  renderer: 'the View makes its renderer - read it from view.renderer',
@@ -213,7 +215,7 @@ export class View {
213
215
  // the renderer checks these too; checked here, the error names the View
214
216
  if (options.exposure !== undefined) checkRange('View', 'exposure', options.exposure, 0)
215
217
  const shared = context === 'shared' || (context === 'auto' && ownContexts >= OWN_CONTEXTS)
216
- this.renderer = new WebGLRenderer({ canvas: this.canvas, shared, antialias: options.antialias, toneMapping: options.toneMapping, exposure: options.exposure, bloom: options.bloom })
218
+ this.renderer = new WebGLRenderer({ canvas: this.canvas, shared, antialias: options.antialias, outlineAntialias: options.outlineAntialias, toneMapping: options.toneMapping, exposure: options.exposure, bloom: options.bloom })
217
219
  if (!shared) {
218
220
  this.#ownContext = true
219
221
  ownContexts++
@@ -6,11 +6,13 @@ import { checkOptions, describe, fail } from '../validate.js'
6
6
  * @property {ArrayLike<number>} positions xyz per vertex
7
7
  * @property {ArrayLike<number>} [normals] xyz per vertex; computed (smooth) when missing
8
8
  * @property {ArrayLike<number>} [uvs] uv per vertex - glTF convention, (0, 0) is the image's top left
9
+ * @property {ArrayLike<number>} [uvs2] a second uv per vertex, for maps that need every surface on its own
10
+ * patch of texture - baked occlusion and light maps (a texture's uvSet: 1 reads it)
9
11
  * @property {ArrayLike<number>} [colors] rgb per vertex, sRGB 0-1 like every colour input
10
12
  * @property {ArrayLike<number>} [indices] vertex indices; triangles for meshes, pairs for lines
11
13
  */
12
14
 
13
- const OPTIONS = ['positions', 'normals', 'uvs', 'colors', 'indices']
15
+ const OPTIONS = ['positions', 'normals', 'uvs', 'uvs2', 'colors', 'indices']
14
16
 
15
17
  /**
16
18
  * Vertex data: flat typed arrays plus an optional index. Generators such as box() and sphere()
@@ -21,6 +23,7 @@ export class Geometry {
21
23
  static OPTIONS = OPTIONS
22
24
  /** @type {Float32Array} */ positions
23
25
  /** @type {Float32Array | null} */ uvs = null
26
+ /** A second UV set, for baked occlusion and light maps. @type {Float32Array | null} */ uvs2 = null
24
27
  /** @type {Float32Array | null} */ colors = null
25
28
  /** @type {Uint16Array | Uint32Array | null} */ indices = null
26
29
 
@@ -55,6 +58,7 @@ export class Geometry {
55
58
  const n = this.vertexCount
56
59
  if (options.normals) this.#normals = toFloat32('normals', options.normals, 3, n)
57
60
  if (options.uvs) this.uvs = toFloat32('uvs', options.uvs, 2, n)
61
+ if (options.uvs2) this.uvs2 = toFloat32('uvs2', options.uvs2, 2, n)
58
62
  if (options.colors) this.colors = toFloat32('colors', options.colors, 3, n)
59
63
  if (options.indices) {
60
64
  // -1: no indices at all refer to no vertex, so an empty geometry can say so
@@ -172,6 +176,7 @@ export class Geometry {
172
176
  positions: this.positions,
173
177
  normals: this.#normals ?? undefined,
174
178
  uvs: this.uvs ?? undefined,
179
+ uvs2: this.uvs2 ?? undefined,
175
180
  colors: this.colors ?? undefined,
176
181
  indices: this.indices ?? undefined,
177
182
  })
@@ -198,6 +203,7 @@ export class Geometry {
198
203
  const json = { positions: Array.from(this.positions) }
199
204
  if (this.#normals) json.normals = Array.from(this.#normals)
200
205
  if (this.uvs) json.uvs = Array.from(this.uvs)
206
+ if (this.uvs2) json.uvs2 = Array.from(this.uvs2)
201
207
  if (this.colors) json.colors = Array.from(this.colors)
202
208
  if (this.indices) json.indices = Array.from(this.indices)
203
209
  return json
@@ -221,6 +227,7 @@ export class Geometry {
221
227
  }
222
228
  this.positions = /** @type {Float32Array} */ (expand(this.positions, 3))
223
229
  this.uvs = expand(this.uvs, 2)
230
+ this.uvs2 = expand(this.uvs2, 2)
224
231
  this.colors = expand(this.colors, 3)
225
232
  this.indices = null
226
233
  this.source = null
package/src/gltf/load.js CHANGED
@@ -25,7 +25,8 @@ import { Animation } from './Animation.js'
25
25
  */
26
26
 
27
27
  const SUPPORTED = new Set(['KHR_materials_emissive_strength', 'KHR_texture_transform', 'KHR_lights_punctual',
28
- 'KHR_materials_unlit', 'KHR_mesh_quantization'])
28
+ 'KHR_materials_unlit', 'KHR_mesh_quantization', 'KHR_materials_transmission', 'KHR_materials_clearcoat', 'KHR_materials_sheen',
29
+ 'KHR_materials_ior', 'KHR_materials_specular', 'KHR_materials_anisotropy', 'KHR_materials_diffuse_transmission'])
29
30
  const COMPRESSION = new Set(['KHR_draco_mesh_compression', 'EXT_meshopt_compression', 'KHR_texture_basisu'])
30
31
 
31
32
  const COMPONENTS = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 }
@@ -266,6 +267,7 @@ class Builder {
266
267
  const options = { positions: this.#accessor(a.POSITION) }
267
268
  if (a.NORMAL !== undefined) options.normals = this.#accessor(a.NORMAL)
268
269
  if (a.TEXCOORD_0 !== undefined) options.uvs = this.#accessor(a.TEXCOORD_0)
270
+ if (a.TEXCOORD_1 !== undefined) options.uvs2 = this.#accessor(a.TEXCOORD_1)
269
271
  if (a.COLOR_0 !== undefined) options.colors = srgbColors(/** @type {Float32Array} */ (this.#accessor(a.COLOR_0)), COMPONENTS[this.json.accessors[a.COLOR_0].type])
270
272
  if (p.indices !== undefined) options.indices = this.#accessor(p.indices, true)
271
273
  const geometry = new Geometry(options)
@@ -364,6 +366,43 @@ class Builder {
364
366
  }
365
367
  material.emissiveIntensity = m.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1
366
368
  if (m.extensions?.KHR_materials_unlit) material.unlit = true
369
+ // glass, clear coat, cloth, reflectance, brushed metal and translucency, by their factors - their
370
+ // own textures aren't read
371
+ const clamp = (/** @type {number} */ v) => Math.min(Math.max(v, 0), 1)
372
+ const x = m.extensions ?? {}
373
+ const transmission = x.KHR_materials_transmission, coat = x.KHR_materials_clearcoat, sheen = x.KHR_materials_sheen
374
+ if (transmission) material.transmission = clamp(transmission.transmissionFactor ?? 0)
375
+ if (coat) {
376
+ material.clearcoat = clamp(coat.clearcoatFactor ?? 0)
377
+ material.clearcoatRoughness = clamp(coat.clearcoatRoughnessFactor ?? 0)
378
+ }
379
+ if (sheen) {
380
+ const c = sheen.sheenColorFactor ?? [0, 0, 0]
381
+ // glTF's sheen is its colour alone: black is none
382
+ if (Math.max(c[0], c[1], c[2]) > 0) {
383
+ material.sheen = 1
384
+ material.sheenColor.setLinear(c[0], c[1], c[2])
385
+ }
386
+ material.sheenRoughness = clamp(sheen.sheenRoughnessFactor ?? 0)
387
+ }
388
+ if (x.KHR_materials_ior?.ior >= 1) material.ior = x.KHR_materials_ior.ior
389
+ const specular = x.KHR_materials_specular
390
+ if (specular) {
391
+ material.specularIntensity = clamp(specular.specularFactor ?? 1)
392
+ const c = specular.specularColorFactor ?? [1, 1, 1]
393
+ material.specularColor.setLinear(c[0], c[1], c[2])
394
+ }
395
+ const brushed = x.KHR_materials_anisotropy
396
+ if (brushed) {
397
+ material.anisotropy = clamp(brushed.anisotropyStrength ?? 0)
398
+ material.anisotropyRotation = brushed.anisotropyRotation ?? 0
399
+ }
400
+ const translucent = x.KHR_materials_diffuse_transmission
401
+ if (translucent) {
402
+ material.translucency = clamp(translucent.diffuseTransmissionFactor ?? 0)
403
+ const c = translucent.diffuseTransmissionColorFactor ?? [1, 1, 1]
404
+ material.translucencyColor.setLinear(c[0], c[1], c[2])
405
+ }
367
406
  if (m.alphaMode === 'BLEND') {
368
407
  material.opacity = base[3]
369
408
  material.transparent = true
@@ -390,10 +429,12 @@ class Builder {
390
429
  */
391
430
  #texture(info, colorSpace) {
392
431
  const transform = info.extensions?.KHR_texture_transform
393
- const texCoord = transform?.texCoord ?? info.texCoord ?? 0
394
- if (texCoord !== 0) warnOnce(this.model, 'textures on a second UV set (TEXCOORD_1) are drawn with the first')
395
- if (transform?.rotation) warnOnce(this.model, "rotated texture transforms (KHR_texture_transform rotation) aren't supported - drawn unrotated")
396
- const key = JSON.stringify([info.index, colorSpace, transform?.offset, transform?.scale])
432
+ let texCoord = transform?.texCoord ?? info.texCoord ?? 0
433
+ if (texCoord > 1) {
434
+ warnOnce(this.model, `textures on a third UV set or later (TEXCOORD_${texCoord}) are drawn with the first`)
435
+ texCoord = 0
436
+ }
437
+ const key = JSON.stringify([info.index, colorSpace, transform?.offset, transform?.scale, transform?.rotation, texCoord])
397
438
  const cached = this.#textures.get(key)
398
439
  if (cached) return cached
399
440
 
@@ -420,6 +461,8 @@ class Builder {
420
461
  flipY: false,
421
462
  repeat: transform?.scale ?? [1, 1],
422
463
  offset: transform?.offset ?? [0, 0],
464
+ rotation: transform?.rotation ?? 0,
465
+ uvSet: texCoord,
423
466
  })
424
467
  this.#textures.set(key, texture)
425
468
  return texture
@@ -538,5 +581,6 @@ function lineList(g, mode) {
538
581
  function withIndices(g, indices, triangles) {
539
582
  // only normals the file gave - reading g.normals would work them out from the strip's own indices,
540
583
  // taken as a triangle list; the new geometry works them out from its real triangles instead
541
- return new Geometry({ positions: g.positions, normals: triangles && g.hasNormals ? g.normals : undefined, uvs: g.uvs ?? undefined, colors: g.colors ?? undefined, indices })
584
+ return new Geometry({ positions: g.positions, normals: triangles && g.hasNormals ? g.normals : undefined, uvs: g.uvs ?? undefined, uvs2: g.uvs2 ?? undefined,
585
+ colors: g.colors ?? undefined, indices })
542
586
  }