phoria2 1.0.0 → 1.2.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.
- package/README.md +71 -13
- package/dist/bloom.js +1 -1
- package/dist/charts.js +397 -384
- package/dist/charts.js.map +1 -1
- package/dist/core.js +4451 -3568
- package/dist/core.js.map +1 -1
- package/dist/edit.js +67 -67
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +6 -2
- package/dist/explain.js.map +1 -1
- package/dist/gltf.js +211 -166
- package/dist/gltf.js.map +1 -1
- package/dist/motion.js +26 -26
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +49 -49
- package/dist/particles.js.map +1 -1
- package/dist/passes.js +188 -86
- package/dist/passes.js.map +1 -1
- package/dist/phoria2.js +1 -1
- package/dist/picking.js +8 -13
- package/dist/picking.js.map +1 -1
- package/dist/snapshot.js +1 -1
- package/dist/tooltip.js +3 -3
- package/dist/tooltip.js.map +1 -1
- package/dist/types/OrbitControl.d.ts +6 -2
- package/dist/types/Raycaster.d.ts +10 -0
- package/dist/types/View.d.ts +64 -1
- package/dist/types/charts/Chart.d.ts +7 -0
- package/dist/types/explain.d.ts +3 -0
- package/dist/types/frame.d.ts +7 -0
- package/dist/types/geometry/Geometry.d.ts +14 -2
- package/dist/types/keyboard.d.ts +48 -0
- package/dist/types/labels.d.ts +4 -0
- package/dist/types/material/Material.d.ts +125 -0
- package/dist/types/material/Texture.d.ts +37 -5
- package/dist/types/scene/Node.d.ts +19 -14
- package/dist/types/scene/drawables.d.ts +32 -14
- package/dist/types/scene/lights.d.ts +16 -2
- package/dist/types/shadow.d.ts +2 -1
- package/dist/types/types.d.ts +14 -0
- package/dist/types/value/Color.d.ts +0 -4
- package/dist/types/value/Quat.d.ts +4 -2
- package/dist/types/webgl/WebGLRenderer.d.ts +6 -0
- package/dist/types/webgl/passes.d.ts +37 -13
- package/dist/types/webgl/resources.d.ts +24 -2
- package/dist/types/webgl/shaders.d.ts +4 -2
- package/llms-full.txt +153 -15
- package/package.json +4 -1
- package/schema/scene.schema.json +146 -1
- package/src/OrbitControl.js +7 -3
- package/src/Raycaster.js +58 -0
- package/src/View.js +207 -42
- package/src/charts/Chart.js +20 -2
- package/src/explain.js +6 -2
- package/src/frame.js +29 -9
- package/src/geometry/Geometry.js +24 -4
- package/src/gltf/load.js +50 -6
- package/src/keyboard.js +298 -0
- package/src/labels.js +4 -1
- package/src/material/Material.js +104 -19
- package/src/material/Texture.js +116 -11
- package/src/scene/Node.js +20 -9
- package/src/scene/Scene.js +2 -0
- package/src/scene/cameras.js +4 -1
- package/src/scene/drawables.js +48 -12
- package/src/scene/lights.js +40 -6
- package/src/shadow.js +27 -4
- package/src/types.js +6 -0
- package/src/value/Color.js +21 -0
- package/src/value/Quat.js +15 -6
- package/src/watch.js +25 -1
- package/src/webgl/WebGLRenderer.js +51 -12
- package/src/webgl/passes.js +190 -32
- package/src/webgl/picking.js +0 -1
- package/src/webgl/resources.js +89 -8
- package/src/webgl/shaders.js +350 -48
package/src/material/Material.js
CHANGED
|
@@ -30,8 +30,36 @@ import { Texture } from './Texture.js'
|
|
|
30
30
|
* @property {MapInput | null} [aoMap] ambient occlusion in its red channel; darkens environment and ambient light
|
|
31
31
|
* @property {number} [aoMapIntensity] 0-1 (default 1)
|
|
32
32
|
* @property {MapInput | null} [emissiveMap] multiplies the emissive colour
|
|
33
|
+
* @property {MapInput | null} [lightMap] light baked into a texture, added as light from all round - usually on
|
|
34
|
+
* the second UV set (uvSet: 1)
|
|
35
|
+
* @property {number} [lightMapIntensity] (default 1)
|
|
36
|
+
* @property {number} [environmentIntensity] this material's share of the environment's light and reflections:
|
|
37
|
+
* lower for a matte floor in a shiny showroom, higher to make one thing catch the light (default 1)
|
|
33
38
|
* @property {ColorInput} [emissive] glow colour (default black)
|
|
34
39
|
* @property {number} [emissiveIntensity] (default 1)
|
|
40
|
+
* @property {number} [transmission] 0-1: how much light passes through, as through glass (default 0). Unlike
|
|
41
|
+
* opacity, which fades the whole surface, glass keeps its reflections, and shows less of what is behind it
|
|
42
|
+
* towards its edges. Its colour shows in proportion to 1 - transmission
|
|
43
|
+
* @property {number} [clearcoat] 0-1: a clear glossy layer over the surface - car paint, lacquer, varnish (default 0)
|
|
44
|
+
* @property {number} [clearcoatRoughness] 0-1: how glossy the coat is, 0 a mirror (default 0)
|
|
45
|
+
* @property {number} [sheen] 0-1: the soft bright rim of cloth at grazing angles - velvet, felt, upholstery (default 0)
|
|
46
|
+
* @property {ColorInput} [sheenColor] the sheen's colour (default white)
|
|
47
|
+
* @property {number} [sheenRoughness] 0-1: a thin bright rim at 0, a broad soft glow at 1 (default 0.5)
|
|
48
|
+
* @property {number} [ior] index of refraction, 1 or more: how strongly a non-metal reflects - water 1.33, glass and
|
|
49
|
+
* plastic 1.5 (the default), diamond 2.4
|
|
50
|
+
* @property {number} [specularIntensity] 0-1: scales a non-metal's reflections, 0 for none - chalk, clay (default 1)
|
|
51
|
+
* @property {ColorInput} [specularColor] tints a non-metal's reflections face on (default white)
|
|
52
|
+
* @property {number} [anisotropy] 0-1: brushed metal - highlights drawn out along the brushing, which runs along
|
|
53
|
+
* the UVs' u (around the mesh where it has no UVs) (default 0)
|
|
54
|
+
* @property {number} [anisotropyRotation] turns the brushing from the UVs' u, in radians (default 0)
|
|
55
|
+
* @property {number} [translucency] 0-1: light through thin surfaces, lighting them from behind - leaves, paper,
|
|
56
|
+
* lampshades, curtains (default 0)
|
|
57
|
+
* @property {ColorInput} [translucencyColor] tints the light that comes through (default white)
|
|
58
|
+
* @property {MapInput | null} [matcap] a picture of a lit sphere that gives the whole look - clay, wax, chrome,
|
|
59
|
+
* a painted ball - picked by which way the surface faces the camera. Lights, shadows and the environment are
|
|
60
|
+
* ignored; the colour and colour map multiply it
|
|
61
|
+
* @property {number} [toon] cartoon shading in this many flat tones (2 or more) with a hard-edged highlight;
|
|
62
|
+
* 0 (the default) is smooth
|
|
35
63
|
* @property {boolean} [unlit] flat colour that ignores lights (default false)
|
|
36
64
|
* @property {boolean} [flatShading] faceted look from face normals (default false)
|
|
37
65
|
* @property {boolean} [doubleSided] draw back faces too (default false)
|
|
@@ -46,18 +74,26 @@ import { Texture } from './Texture.js'
|
|
|
46
74
|
* @property {number} [width] Lines: width in pixels (default 1)
|
|
47
75
|
*/
|
|
48
76
|
|
|
49
|
-
|
|
77
|
+
// the matcap last: the first map's repeat and offset are every map's, and a matcap has none
|
|
78
|
+
const MAPS = ['map', 'roughnessMap', 'metalnessMap', 'normalMap', 'bumpMap', 'aoMap', 'emissiveMap', 'lightMap', 'matcap']
|
|
50
79
|
// data maps hold numbers, not colours
|
|
51
80
|
const LINEAR_MAPS = new Set(['roughnessMap', 'metalnessMap', 'normalMap', 'bumpMap', 'aoMap'])
|
|
52
81
|
const FLAGS = ['unlit', 'flatShading', 'doubleSided', 'vertexColors', 'depthWrite', 'depthTest', 'sizeAttenuation', 'transparent', 'fog']
|
|
53
82
|
const NUMBERS = { opacity: [0, 1], roughness: [0, 1], metalness: [0, 1], emissiveIntensity: [0, Infinity], size: [0, Infinity], width: [0, Infinity],
|
|
54
|
-
alphaTest: [0, 1], normalScale: [-Infinity, Infinity], aoMapIntensity: [0, 1], triplanar: [0, Infinity], bumpScale: [-Infinity, Infinity]
|
|
83
|
+
alphaTest: [0, 1], normalScale: [-Infinity, Infinity], aoMapIntensity: [0, 1], triplanar: [0, Infinity], bumpScale: [-Infinity, Infinity],
|
|
84
|
+
transmission: [0, 1], clearcoat: [0, 1], clearcoatRoughness: [0, 1], sheen: [0, 1], sheenRoughness: [0, 1], ior: [1, Infinity],
|
|
85
|
+
specularIntensity: [0, 1], anisotropy: [0, 1], anisotropyRotation: [-Infinity, Infinity], translucency: [0, 1], toon: [0, 16],
|
|
86
|
+
lightMapIntensity: [0, Infinity], environmentIntensity: [0, Infinity] }
|
|
87
|
+
// colours, and their defaults
|
|
88
|
+
const COLORS = { color: 0xffffff, emissive: 0x000000, sheenColor: 0xffffff, specularColor: 0xffffff, translucencyColor: 0xffffff }
|
|
55
89
|
const DEFAULTS = {
|
|
56
|
-
name: '', opacity: 1, alphaTest: 0, roughness: 0.5, metalness: 0, normalScale: 1, bumpScale: 1, aoMapIntensity: 1, triplanar: 0, emissiveIntensity: 1,
|
|
90
|
+
name: '', opacity: 1, alphaTest: 0, roughness: 0.5, metalness: 0, normalScale: 1, bumpScale: 1, aoMapIntensity: 1, triplanar: 0, emissiveIntensity: 1,
|
|
91
|
+
transmission: 0, clearcoat: 0, clearcoatRoughness: 0, sheen: 0, sheenRoughness: 0.5, ior: 1.5, specularIntensity: 1, anisotropy: 0,
|
|
92
|
+
anisotropyRotation: 0, translucency: 0, toon: 0, lightMapIntensity: 1, environmentIntensity: 1, unlit: false, flatShading: false,
|
|
57
93
|
doubleSided: false, vertexColors: false, blending: 'normal', depthWrite: true, depthTest: true, fog: true,
|
|
58
94
|
size: 1, sizeAttenuation: true, width: 1,
|
|
59
95
|
}
|
|
60
|
-
const OPTIONS = ['name',
|
|
96
|
+
const OPTIONS = ['name', ...Object.keys(COLORS), ...MAPS, ...FLAGS, ...Object.keys(NUMBERS), 'blending']
|
|
61
97
|
|
|
62
98
|
// what agents (and three.js habits) will reach for
|
|
63
99
|
const HINTS = {
|
|
@@ -65,7 +101,17 @@ const HINTS = {
|
|
|
65
101
|
side: "use 'doubleSided: true'",
|
|
66
102
|
wireframe: 'use new Lines({ geometry: edges(geometry) }) for a wireframe',
|
|
67
103
|
shininess: "use 'roughness' (0 = mirror, 1 = matte)",
|
|
68
|
-
|
|
104
|
+
envMapIntensity: "use 'environmentIntensity'",
|
|
105
|
+
reflectivityScale: "use 'environmentIntensity'",
|
|
106
|
+
matcapMap: "use 'matcap'",
|
|
107
|
+
gradientMap: "use 'toon' - the number of flat tones",
|
|
108
|
+
cel: "use 'toon' - the number of flat tones",
|
|
109
|
+
celShading: "use 'toon' - the number of flat tones",
|
|
110
|
+
specular: "use 'specularIntensity' and 'specularColor' for a non-metal's reflections, or 'roughness' and 'metalness'",
|
|
111
|
+
reflectivity: "use 'ior' (1.5 by default) or 'specularIntensity'",
|
|
112
|
+
anisotropyStrength: "use 'anisotropy'",
|
|
113
|
+
diffuseTransmission: "use 'translucency'",
|
|
114
|
+
subsurface: "use 'translucency' for light through thin surfaces",
|
|
69
115
|
colour: "use 'color'",
|
|
70
116
|
emissiveColor: "use 'emissive'",
|
|
71
117
|
alpha: "use 'opacity'",
|
|
@@ -91,6 +137,19 @@ export class Material {
|
|
|
91
137
|
/** @type {number} */ roughness
|
|
92
138
|
/** @type {number} */ metalness
|
|
93
139
|
/** @type {number} */ emissiveIntensity
|
|
140
|
+
/** How much light passes through, as through glass: 0-1. @type {number} */ transmission
|
|
141
|
+
/** A clear glossy layer over the surface: 0-1. @type {number} */ clearcoat
|
|
142
|
+
/** How glossy the coat is, 0 a mirror. @type {number} */ clearcoatRoughness
|
|
143
|
+
/** The soft bright rim of cloth: 0-1. @type {number} */ sheen
|
|
144
|
+
/** A thin bright rim at 0, a broad soft glow at 1. @type {number} */ sheenRoughness
|
|
145
|
+
/** Index of refraction: how strongly a non-metal reflects (1.5). @type {number} */ ior
|
|
146
|
+
/** Scales a non-metal's reflections, 0-1. @type {number} */ specularIntensity
|
|
147
|
+
/** Brushed metal: 0-1. @type {number} */ anisotropy
|
|
148
|
+
/** Turns the brushing from the UVs' u, in radians. @type {number} */ anisotropyRotation
|
|
149
|
+
/** Light through thin surfaces: 0-1. @type {number} */ translucency
|
|
150
|
+
/** Cartoon shading in this many flat tones; 0 is smooth. @type {number} */ toon
|
|
151
|
+
/** @type {number} */ lightMapIntensity
|
|
152
|
+
/** This material's share of the environment's light and reflections. @type {number} */ environmentIntensity
|
|
94
153
|
/** @type {boolean} */ unlit
|
|
95
154
|
/** @type {boolean} */ flatShading
|
|
96
155
|
/** @type {boolean} */ doubleSided
|
|
@@ -103,8 +162,8 @@ export class Material {
|
|
|
103
162
|
/** @type {boolean} */ sizeAttenuation
|
|
104
163
|
/** @type {number} */ width
|
|
105
164
|
|
|
106
|
-
|
|
107
|
-
#
|
|
165
|
+
/** @type {Record<string, Color>} */
|
|
166
|
+
#colors = Object.fromEntries(Object.entries(COLORS).map(([key, hex]) => [key, new Color(hex)]))
|
|
108
167
|
#transparent = false
|
|
109
168
|
/** @type {Record<string, Texture | null>} */
|
|
110
169
|
#maps = Object.fromEntries(MAPS.map((m) => [m, null]))
|
|
@@ -117,8 +176,7 @@ export class Material {
|
|
|
117
176
|
Object.assign(this, DEFAULTS)
|
|
118
177
|
for (const [key, value] of /** @type {[string, any][]} */ (Object.entries(options))) {
|
|
119
178
|
if (value === undefined) continue
|
|
120
|
-
if (key
|
|
121
|
-
else if (key === 'emissive') this.emissive = value
|
|
179
|
+
if (key in COLORS) this.#colors[key].set(value, 'Material')
|
|
122
180
|
else if (MAPS.includes(key)) this.setMap(key, value)
|
|
123
181
|
else if (FLAGS.includes(key)) this[key] = bool(key, value)
|
|
124
182
|
else if (key in NUMBERS) this[key] = range(key, value, NUMBERS[key])
|
|
@@ -141,14 +199,29 @@ export class Material {
|
|
|
141
199
|
}
|
|
142
200
|
|
|
143
201
|
/** Base colour; assign any colour input. @returns {Color} */
|
|
144
|
-
get color() { return this.#color }
|
|
202
|
+
get color() { return this.#colors.color }
|
|
145
203
|
/** @param {ColorInput} v */
|
|
146
|
-
set color(v) { this.#color.set(v, 'Material') }
|
|
204
|
+
set color(v) { this.#colors.color.set(v, 'Material') }
|
|
147
205
|
|
|
148
206
|
/** Glow colour; assign any colour input. @returns {Color} */
|
|
149
|
-
get emissive() { return this.#emissive }
|
|
207
|
+
get emissive() { return this.#colors.emissive }
|
|
150
208
|
/** @param {ColorInput} v */
|
|
151
|
-
set emissive(v) { this.#emissive.set(v, 'Material') }
|
|
209
|
+
set emissive(v) { this.#colors.emissive.set(v, 'Material') }
|
|
210
|
+
|
|
211
|
+
/** The sheen's colour; assign any colour input. @returns {Color} */
|
|
212
|
+
get sheenColor() { return this.#colors.sheenColor }
|
|
213
|
+
/** @param {ColorInput} v */
|
|
214
|
+
set sheenColor(v) { this.#colors.sheenColor.set(v, 'Material') }
|
|
215
|
+
|
|
216
|
+
/** Tints a non-metal's reflections face on; assign any colour input. @returns {Color} */
|
|
217
|
+
get specularColor() { return this.#colors.specularColor }
|
|
218
|
+
/** @param {ColorInput} v */
|
|
219
|
+
set specularColor(v) { this.#colors.specularColor.set(v, 'Material') }
|
|
220
|
+
|
|
221
|
+
/** Tints the light that comes through a translucent surface; assign any colour input. @returns {Color} */
|
|
222
|
+
get translucencyColor() { return this.#colors.translucencyColor }
|
|
223
|
+
/** @param {ColorInput} v */
|
|
224
|
+
set translucencyColor(v) { this.#colors.translucencyColor.set(v, 'Material') }
|
|
152
225
|
|
|
153
226
|
/** @returns {Texture | null} */
|
|
154
227
|
get map() { return this.#maps.map }
|
|
@@ -178,9 +251,17 @@ export class Material {
|
|
|
178
251
|
get emissiveMap() { return this.#maps.emissiveMap }
|
|
179
252
|
/** @param {MapInput | null} v */
|
|
180
253
|
set emissiveMap(v) { this.setMap('emissiveMap', v) }
|
|
254
|
+
/** Light baked into a texture. @returns {Texture | null} */
|
|
255
|
+
get lightMap() { return this.#maps.lightMap }
|
|
256
|
+
/** @param {MapInput | null} v */
|
|
257
|
+
set lightMap(v) { this.setMap('lightMap', v) }
|
|
258
|
+
/** A picture of a lit sphere that gives the whole look. @returns {Texture | null} */
|
|
259
|
+
get matcap() { return this.#maps.matcap }
|
|
260
|
+
/** @param {MapInput | null} v */
|
|
261
|
+
set matcap(v) { this.setMap('matcap', v) }
|
|
181
262
|
|
|
182
263
|
/** @internal
|
|
183
|
-
* @param {string} slot one of map, roughnessMap, metalnessMap, normalMap, aoMap, emissiveMap
|
|
264
|
+
* @param {string} slot one of map, roughnessMap, metalnessMap, normalMap, bumpMap, aoMap, emissiveMap, lightMap, matcap
|
|
184
265
|
* @param {MapInput | null} value
|
|
185
266
|
*/
|
|
186
267
|
setMap(slot, value) {
|
|
@@ -213,7 +294,7 @@ export class Material {
|
|
|
213
294
|
|
|
214
295
|
/** Rendered in the transparent pass: an opacity below 1, additive or multiply blending, or set for maps with soft alpha. @returns {boolean} */
|
|
215
296
|
get transparent() {
|
|
216
|
-
return this.#transparent || this.opacity < 1 || this.blending !== 'normal'
|
|
297
|
+
return this.#transparent || this.opacity < 1 || this.transmission > 0 || this.blending !== 'normal'
|
|
217
298
|
}
|
|
218
299
|
/** @param {boolean} v */
|
|
219
300
|
set transparent(v) { this.#transparent = !!v }
|
|
@@ -226,6 +307,12 @@ export class Material {
|
|
|
226
307
|
const parts = [this.unlit ? 'unlit' : 'lit']
|
|
227
308
|
for (const [slot] of this.textures) parts.push(slot)
|
|
228
309
|
if (this.alphaTest > 0) parts.push('cutout')
|
|
310
|
+
if (this.transmission > 0) parts.push('glass')
|
|
311
|
+
if (this.clearcoat > 0) parts.push('clearcoat')
|
|
312
|
+
if (this.sheen > 0) parts.push('sheen')
|
|
313
|
+
if (this.anisotropy > 0) parts.push('brushed')
|
|
314
|
+
if (this.translucency > 0) parts.push('translucent')
|
|
315
|
+
if (this.toon > 0) parts.push('toon')
|
|
229
316
|
if (this.flatShading) parts.push('flat')
|
|
230
317
|
if (this.vertexColors) parts.push('vcolor')
|
|
231
318
|
return parts.join('+')
|
|
@@ -240,8 +327,7 @@ export class Material {
|
|
|
240
327
|
clone() {
|
|
241
328
|
const copy = new Material(this.toJSON(true))
|
|
242
329
|
// the colours exactly: a description rounds them to 8-bit sRGB and can't hold values past 1
|
|
243
|
-
|
|
244
|
-
copy.emissive.set(this.#emissive)
|
|
330
|
+
for (const [key, c] of Object.entries(this.#colors)) copy.#colors[key].set(c)
|
|
245
331
|
return copy
|
|
246
332
|
}
|
|
247
333
|
|
|
@@ -268,8 +354,7 @@ export class Material {
|
|
|
268
354
|
if (this[key] !== value) json[key] = this[key]
|
|
269
355
|
}
|
|
270
356
|
if (this.#transparent) json.transparent = true
|
|
271
|
-
|
|
272
|
-
if (this.#emissive.getHex() !== 0) json.emissive = this.#emissive.getStyle()
|
|
357
|
+
for (const [key, c] of Object.entries(this.#colors)) if (c.getHex() !== COLORS[key]) json[key] = c.getStyle()
|
|
273
358
|
for (const [slot, t] of this.textures) {
|
|
274
359
|
const desc = keepTextures ? t : t.toJSON()
|
|
275
360
|
if (desc) json[slot] = desc
|
package/src/material/Texture.js
CHANGED
|
@@ -10,36 +10,49 @@ import { checkNumber, checkOneOf, checkOptions, describe, fail } from '../valida
|
|
|
10
10
|
* @typedef {object} TextureOptions
|
|
11
11
|
* @property {TextureImage} [image] the pixels; omit and give a url to load them
|
|
12
12
|
* @property {string} [url] image to load (starts loading immediately in a browser); a .hdr file loads as
|
|
13
|
-
* linear HDR radiance, for scene.environment
|
|
13
|
+
* linear HDR radiance, for scene.environment; a video (.mp4, .webm, .ogv, .mov, .m4v) plays muted and on a
|
|
14
|
+
* loop, and texture.image is its <video> to pause, seek or unmute
|
|
14
15
|
* @property {'srgb' | 'linear'} [colorSpace] 'srgb' for colour maps (default), 'linear' for data
|
|
15
16
|
* maps such as normal and roughness maps
|
|
16
17
|
* @property {'repeat' | 'clamp' | 'mirror'} [wrap] (default 'repeat')
|
|
17
18
|
* @property {number[]} [repeat] [u, v] (default [1, 1])
|
|
18
19
|
* @property {number[]} [offset] [u, v] (default [0, 0])
|
|
20
|
+
* @property {number} [rotation] turns the image on the surface, in radians, about `center` (default 0)
|
|
21
|
+
* @property {number[]} [center] [u, v] the point rotation turns about, in repeated UVs (default [0, 0], the
|
|
22
|
+
* corner, as glTF has it; [0.5, 0.5] for the middle of an unrepeated image)
|
|
23
|
+
* @property {0 | 1} [uvSet] which UV set to read: 0 the first, 1 the second (Geometry uvs2) - baked occlusion
|
|
24
|
+
* and light maps usually come on the second (default 0)
|
|
19
25
|
* @property {'linear' | 'nearest'} [filter] (default 'linear')
|
|
20
|
-
* @property {boolean} [mipmaps] (default true)
|
|
26
|
+
* @property {boolean} [mipmaps] (default true; false for a video, which would make them every frame)
|
|
21
27
|
* @property {number} [anisotropy] samples along a surface seen at a glancing angle - floors and roads
|
|
22
28
|
* stay sharp into the distance. Up to what the GPU allows; 1 is off (default 8)
|
|
23
29
|
* @property {boolean} [flipY] flip on upload; false for glTF-style UVs (default false)
|
|
24
30
|
*/
|
|
25
31
|
|
|
26
|
-
const OPTIONS = ['image', 'url', 'colorSpace', 'wrap', 'repeat', 'offset', 'filter', 'mipmaps', 'anisotropy', 'flipY']
|
|
27
|
-
const DEFAULTS = { colorSpace: 'srgb', wrap: 'repeat', repeat: [1, 1], offset: [0, 0],
|
|
32
|
+
const OPTIONS = ['image', 'url', 'colorSpace', 'wrap', 'repeat', 'offset', 'rotation', 'center', 'uvSet', 'filter', 'mipmaps', 'anisotropy', 'flipY']
|
|
33
|
+
const DEFAULTS = { colorSpace: 'srgb', wrap: 'repeat', repeat: [1, 1], offset: [0, 0], rotation: 0, center: [0, 0], uvSet: 0, filter: 'linear',
|
|
34
|
+
mipmaps: true, anisotropy: 8, flipY: false }
|
|
28
35
|
|
|
29
36
|
const HINTS = {
|
|
30
37
|
src: "use 'url' for the image address",
|
|
31
38
|
encoding: "use colorSpace: 'srgb' | 'linear'",
|
|
32
39
|
wrapS: "use wrap: 'repeat' | 'clamp' | 'mirror' (applies to both directions)",
|
|
33
40
|
maxAnisotropy: "use 'anisotropy' - samples along glancing angles, 1 for off",
|
|
41
|
+
channel: "use 'uvSet' - 0 for the first UV set, 1 for the second",
|
|
42
|
+
texCoord: "use 'uvSet' - 0 for the first UV set, 1 for the second",
|
|
43
|
+
scale: "use 'repeat' - [u, v] times across",
|
|
34
44
|
}
|
|
35
45
|
|
|
36
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* An image for a material map. A video's frames upload by themselves as it plays; after drawing on
|
|
48
|
+
* a canvas, call `needsUpdate()`.
|
|
49
|
+
*/
|
|
37
50
|
export class Texture {
|
|
38
51
|
/** The options it takes, and hints for names people reach for instead. */
|
|
39
52
|
static OPTIONS = OPTIONS
|
|
40
53
|
static HINTS = HINTS
|
|
41
|
-
/** @type {TextureImage | null}
|
|
42
|
-
image = null
|
|
54
|
+
/** @type {TextureImage | null} */
|
|
55
|
+
#image = null
|
|
43
56
|
/** @type {string | null} */
|
|
44
57
|
url = null
|
|
45
58
|
/** @type {'srgb' | 'linear'} */
|
|
@@ -50,6 +63,12 @@ export class Texture {
|
|
|
50
63
|
wrap = 'repeat'
|
|
51
64
|
repeat = [1, 1]
|
|
52
65
|
offset = [0, 0]
|
|
66
|
+
/** Turns the image on the surface, in radians, about `center`. */
|
|
67
|
+
rotation = 0
|
|
68
|
+
/** [u, v] the point rotation turns about, in repeated UVs. */
|
|
69
|
+
center = [0, 0]
|
|
70
|
+
/** Which UV set it reads: 0 the first, 1 the second (Geometry uvs2). @type {0 | 1} */
|
|
71
|
+
uvSet = 0
|
|
53
72
|
/** @type {'linear' | 'nearest'} */
|
|
54
73
|
filter = 'linear'
|
|
55
74
|
mipmaps = true
|
|
@@ -75,7 +94,12 @@ export class Texture {
|
|
|
75
94
|
this.filter = checkOneOf('Texture', 'filter', o.filter, ['linear', 'nearest'])
|
|
76
95
|
this.repeat = pair('repeat', o.repeat)
|
|
77
96
|
this.offset = pair('offset', o.offset)
|
|
78
|
-
this.
|
|
97
|
+
this.rotation = checkNumber('Texture', 'rotation', o.rotation)
|
|
98
|
+
this.center = pair('center', o.center)
|
|
99
|
+
if (o.uvSet !== 0 && o.uvSet !== 1) fail('Texture', `'uvSet' is 0 (the first UV set) or 1 (the second), got ${describe(o.uvSet)}`)
|
|
100
|
+
this.uvSet = o.uvSet
|
|
101
|
+
// a video would make mipmaps every frame: off unless asked for
|
|
102
|
+
this.mipmaps = options.mipmaps !== undefined ? !!options.mipmaps : !(isVideo(o.image) || (!o.image && VIDEO.test(o.url ?? '')))
|
|
79
103
|
this.anisotropy = checkNumber('Texture', 'anisotropy', o.anisotropy)
|
|
80
104
|
if (!(this.anisotropy >= 1)) fail('Texture', `'anisotropy' is how many samples along a glancing angle - 1 (off) or more, got ${this.anisotropy}`)
|
|
81
105
|
this.flipY = !!o.flipY
|
|
@@ -83,7 +107,8 @@ export class Texture {
|
|
|
83
107
|
|
|
84
108
|
if (o.image) {
|
|
85
109
|
this.image = o.image
|
|
86
|
-
this.ready = Promise.resolve(this)
|
|
110
|
+
this.ready = isVideo(o.image) ? loaded(/** @type {HTMLVideoElement} */ (o.image)).then(() => this) : Promise.resolve(this)
|
|
111
|
+
this.ready.catch(() => {})
|
|
87
112
|
} else if (this.url) {
|
|
88
113
|
this.ready = this.#load(this.url)
|
|
89
114
|
// a failed load is reported by whoever awaits `ready`; don't also raise an unhandled rejection
|
|
@@ -94,6 +119,57 @@ export class Texture {
|
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
/** Call after redrawing a canvas or advancing a video. @returns {this} */
|
|
122
|
+
/**
|
|
123
|
+
* The pixels: null until loaded. A <video>'s frames upload by themselves while it plays.
|
|
124
|
+
* @returns {TextureImage | null}
|
|
125
|
+
*/
|
|
126
|
+
get image() { return this.#image }
|
|
127
|
+
/** @param {TextureImage | null} v */
|
|
128
|
+
set image(v) {
|
|
129
|
+
this.#image = v
|
|
130
|
+
this.needsUpdate()
|
|
131
|
+
if (isVideo(v)) this.#follow(/** @type {HTMLVideoElement} */ (v))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Each new frame of a playing video marks the texture changed, so views redraw with it: the
|
|
135
|
+
// browser's own per-frame callback where there is one, otherwise a check every animation frame
|
|
136
|
+
// while it plays. Following stops when the image is replaced.
|
|
137
|
+
/** @param {HTMLVideoElement} video */
|
|
138
|
+
#follow(video) {
|
|
139
|
+
if (typeof video.requestVideoFrameCallback === 'function') {
|
|
140
|
+
const frame = () => {
|
|
141
|
+
if (this.#image !== video) return
|
|
142
|
+
this.needsUpdate()
|
|
143
|
+
video.requestVideoFrameCallback(frame)
|
|
144
|
+
}
|
|
145
|
+
video.requestVideoFrameCallback(frame)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
let last = -1, running = false
|
|
149
|
+
const tick = () => {
|
|
150
|
+
if (this.#image !== video || video.paused || video.ended) {
|
|
151
|
+
running = false
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if (video.currentTime !== last) {
|
|
155
|
+
last = video.currentTime
|
|
156
|
+
this.needsUpdate()
|
|
157
|
+
}
|
|
158
|
+
requestAnimationFrame(tick)
|
|
159
|
+
}
|
|
160
|
+
const start = () => {
|
|
161
|
+
if (running || this.#image !== video) return
|
|
162
|
+
running = true
|
|
163
|
+
requestAnimationFrame(tick)
|
|
164
|
+
}
|
|
165
|
+
video.addEventListener('play', start)
|
|
166
|
+
// a seek while paused shows a new frame too
|
|
167
|
+
video.addEventListener('seeked', () => {
|
|
168
|
+
if (this.#image === video) this.needsUpdate()
|
|
169
|
+
})
|
|
170
|
+
if (!video.paused) start()
|
|
171
|
+
}
|
|
172
|
+
|
|
97
173
|
/** 'srgb' for colour (decoded to linear by the GPU), 'linear' for data such as normals and roughness. Call needsUpdate() after changing it. @returns {'srgb' | 'linear'} */
|
|
98
174
|
get colorSpace() { return this.#colorSpace }
|
|
99
175
|
set colorSpace(v) {
|
|
@@ -139,12 +215,26 @@ export class Texture {
|
|
|
139
215
|
|
|
140
216
|
/** @param {string} url @returns {Promise<Texture>} */
|
|
141
217
|
async #load(url) {
|
|
218
|
+
if (VIDEO.test(url)) {
|
|
219
|
+
if (typeof document === 'undefined') throw new Error(`Texture: can't load '${url}' - no video support in this environment`)
|
|
220
|
+
const video = document.createElement('video')
|
|
221
|
+
// muted, so browsers let it play by itself; inline, so phones don't take it full screen
|
|
222
|
+
Object.assign(video, { crossOrigin: 'anonymous', muted: true, loop: true, playsInline: true, autoplay: true, src: url })
|
|
223
|
+
try {
|
|
224
|
+
await loaded(video)
|
|
225
|
+
} catch {
|
|
226
|
+
throw new Error(`Texture: failed to load '${url}'`)
|
|
227
|
+
}
|
|
228
|
+
// a browser that won't autoplay leaves it paused on its first frame: video.play() on a click starts it
|
|
229
|
+
video.play()?.catch(() => {})
|
|
230
|
+
this.image = video
|
|
231
|
+
return this
|
|
232
|
+
}
|
|
142
233
|
if (/\.hdr($|[?#])/i.test(url)) {
|
|
143
234
|
const [{ parseHDR }, response] = await Promise.all([import('../hdr.js'), fetch(url)])
|
|
144
235
|
if (!response.ok) throw new Error(`Texture: failed to load '${url}' (${response.status})`)
|
|
145
236
|
this.image = parseHDR(await response.arrayBuffer())
|
|
146
237
|
this.colorSpace = 'linear'
|
|
147
|
-
this.needsUpdate()
|
|
148
238
|
return this
|
|
149
239
|
}
|
|
150
240
|
if (typeof Image === 'undefined') {
|
|
@@ -159,11 +249,26 @@ export class Texture {
|
|
|
159
249
|
throw new Error(`Texture: failed to load '${url}'`)
|
|
160
250
|
}
|
|
161
251
|
this.image = img
|
|
162
|
-
this.needsUpdate()
|
|
163
252
|
return this
|
|
164
253
|
}
|
|
165
254
|
}
|
|
166
255
|
|
|
256
|
+
const VIDEO = /\.(mp4|webm|ogv|mov|m4v)($|[?#])/i
|
|
257
|
+
|
|
258
|
+
/** @internal A <video>, whose frames change as it plays. @param {unknown} image @returns {boolean} */
|
|
259
|
+
export function isVideo(image) {
|
|
260
|
+
return typeof HTMLVideoElement !== 'undefined' && image instanceof HTMLVideoElement
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Resolves once a video has a frame to show. @param {HTMLVideoElement} video @returns {Promise<void>} */
|
|
264
|
+
function loaded(video) {
|
|
265
|
+
if (video.readyState >= 2) return Promise.resolve()
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
video.addEventListener('loadeddata', () => resolve(), { once: true })
|
|
268
|
+
video.addEventListener('error', () => reject(new Error('Texture: the video failed to load')), { once: true })
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
167
272
|
/**
|
|
168
273
|
* Load an image as a texture; resolves once it has decoded.
|
|
169
274
|
* @param {string} url
|
package/src/scene/Node.js
CHANGED
|
@@ -10,7 +10,7 @@ import { Vec3 } from '../value/Vec3.js'
|
|
|
10
10
|
import { checkOptions, describe, fail } from '../validate.js'
|
|
11
11
|
import { Clip } from '../clip.js'
|
|
12
12
|
|
|
13
|
-
/** @import { ColorInput, Vec3Input } from '../types.js' */
|
|
13
|
+
/** @import { ColorInput, QuatInput, Vec3Input } from '../types.js' */
|
|
14
14
|
/** @import { Hit } from '../Raycaster.js' */
|
|
15
15
|
/** @import { Camera } from './cameras.js' */
|
|
16
16
|
/** @import { ClipOptions } from '../clip.js' */
|
|
@@ -44,7 +44,7 @@ import { Clip } from '../clip.js'
|
|
|
44
44
|
* @property {string} [name]
|
|
45
45
|
* @property {Vec3Input} [position]
|
|
46
46
|
* @property {Vec3Input | Euler} [rotation] Euler angles in radians, XYZ order
|
|
47
|
-
* @property {
|
|
47
|
+
* @property {QuatInput} [quaternion] [x, y, z, w] or { x, y, z, w } - instead of rotation
|
|
48
48
|
* @property {Vec3Input | number} [scale] a number scales uniformly
|
|
49
49
|
* @property {boolean} [visible]
|
|
50
50
|
* @property {boolean} [pickable] include in view.pick() (default true)
|
|
@@ -54,7 +54,8 @@ import { Clip } from '../clip.js'
|
|
|
54
54
|
* @property {UpdateHandler | UpdateHandler[]} [onUpdate]
|
|
55
55
|
* @property {((hit: Hit) => any) | null} [tooltip] tooltip for anything picked in this subtree; nearer nodes win, and
|
|
56
56
|
* all win over the View's
|
|
57
|
-
* @property {ColorInput | null} [highlight] hover tint for this subtree, over the View's
|
|
57
|
+
* @property {ColorInput | null | false} [highlight] hover tint for this subtree, over the View's; false for none - a
|
|
58
|
+
* floor or wall left plain under a View that tints everything else (null: the View's)
|
|
58
59
|
* @property {boolean} [highlightWhole] hovering any part of this subtree tints all of it, as one thing - a
|
|
59
60
|
* piece of furniture, a machine - rather than just the part under the pointer (default false)
|
|
60
61
|
* @property {((hit: Hit | null) => void) | null} [onHover] called as the thing under the pointer in this subtree changes
|
|
@@ -136,7 +137,7 @@ export class Node {
|
|
|
136
137
|
onDrag = null
|
|
137
138
|
/** @type {Outline | null} */
|
|
138
139
|
#outline = null
|
|
139
|
-
/** @type {Color | null} */
|
|
140
|
+
/** @type {Color | null | false} */
|
|
140
141
|
#highlight = null
|
|
141
142
|
/** Hovering any part of this subtree tints all of it, not just the part under the pointer. */
|
|
142
143
|
highlightWhole = false
|
|
@@ -164,7 +165,7 @@ export class Node {
|
|
|
164
165
|
set rotation(v) { this.#rotation.copy(/** @type {any} */ (v), this.constructor.name) }
|
|
165
166
|
/** Kept in sync with rotation. Assigning copies, normalised. @returns {Quat} */
|
|
166
167
|
get quaternion() { return this.#quaternion }
|
|
167
|
-
/** @param {
|
|
168
|
+
/** @param {QuatInput} v */
|
|
168
169
|
set quaternion(v) { this.#quaternion.copy(v, this.constructor.name).normalize() }
|
|
169
170
|
|
|
170
171
|
/** @param {NodeOptions} [options] */
|
|
@@ -191,7 +192,7 @@ export class Node {
|
|
|
191
192
|
if (typeof fn !== 'function') fail(owner, `'${key}' must be a function, got ${describe(fn)}`)
|
|
192
193
|
this[key] = fn
|
|
193
194
|
}
|
|
194
|
-
if (options.highlight != null) this.#
|
|
195
|
+
if (options.highlight != null) this.#setHighlight(options.highlight, owner)
|
|
195
196
|
if (options.highlightWhole !== undefined) {
|
|
196
197
|
if (typeof options.highlightWhole !== 'boolean') fail(owner, `'highlightWhole' must be true or false, got ${describe(options.highlightWhole)}`)
|
|
197
198
|
this.highlightWhole = options.highlightWhole
|
|
@@ -201,14 +202,23 @@ export class Node {
|
|
|
201
202
|
if (options.children) for (const child of options.children) this.add(child)
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* Hover tint for this subtree, over the View's: a colour, false for none, or null to use the View's.
|
|
207
|
+
* @returns {Color | null | false}
|
|
208
|
+
*/
|
|
205
209
|
get highlight() {
|
|
206
210
|
return this.#highlight
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
/** @param {ColorInput | null} value */
|
|
213
|
+
/** @param {ColorInput | null | false} value */
|
|
210
214
|
set highlight(value) {
|
|
211
|
-
this.#
|
|
215
|
+
this.#setHighlight(value, this.constructor.name)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @param {ColorInput | null | false | true} value @param {string} owner */
|
|
219
|
+
#setHighlight(value, owner) {
|
|
220
|
+
if (value === true) fail(owner, "'highlight' is a colour, false for no tint, or null to use the View's - got true")
|
|
221
|
+
this.#highlight = value == null ? null : value === false ? false : Color.fromInput(value, owner)
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
/**
|
|
@@ -612,6 +622,7 @@ export class Node {
|
|
|
612
622
|
if (!this.pickable) json.pickable = false
|
|
613
623
|
if (!this.matrixAutoUpdate) json.matrixAutoUpdate = false
|
|
614
624
|
if (this.highlight) json.highlight = this.highlight.getStyle()
|
|
625
|
+
else if (this.highlight === false) json.highlight = false
|
|
615
626
|
if (this.highlightWhole) json.highlightWhole = true
|
|
616
627
|
if (this.#clip) json.clip = this.#clip.toJSON()
|
|
617
628
|
if (this.#outline) json.outline = { color: this.#outline.color.getStyle(), width: this.#outline.width }
|
package/src/scene/Scene.js
CHANGED
|
@@ -44,6 +44,8 @@ export class Scene extends Node {
|
|
|
44
44
|
environmentIntensity = 1
|
|
45
45
|
/** 0-1: how blurred a panorama background is drawn - a soft backdrop that doesn't compete. */
|
|
46
46
|
backgroundBlur = 0
|
|
47
|
+
/** @internal Set by the View drawing it, from its reducedMotion: charts' transitions and flows stand still. */
|
|
48
|
+
motionReduced = false
|
|
47
49
|
/** @type {Fog | null} */
|
|
48
50
|
#fog = null
|
|
49
51
|
|
package/src/scene/cameras.js
CHANGED
|
@@ -20,7 +20,10 @@ const tmp3 = new Float32Array(9)
|
|
|
20
20
|
export function aimAt(node, target) {
|
|
21
21
|
if (node.position.equals(target, 1e-9)) return
|
|
22
22
|
const dir = target.clone().sub(node.position).normalize()
|
|
23
|
-
|
|
23
|
+
// world up keeps the picture upright, and turns it smoothly with the view right up to straight up
|
|
24
|
+
// or down - the orbit stops a thousandth of a radian short. Only within a hair of exactly vertical
|
|
25
|
+
// (the 'top' preset's view), where up would no longer say which way the screen faces, is north up
|
|
26
|
+
const up = Math.hypot(dir[0], dir[2]) < 5e-4 ? [0, 0, -1] : UP
|
|
24
27
|
mat4.targetTo(tmp, node.position, target, up)
|
|
25
28
|
const q = quat.normalize(new Float32Array(4), quat.fromMat3(new Float32Array(4), mat3.fromMat4(tmp3, tmp)))
|
|
26
29
|
node.quaternion.copy(q)
|