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/scene/drawables.js
CHANGED
|
@@ -12,7 +12,7 @@ import { NODE_OPTIONS, Node } from './Node.js'
|
|
|
12
12
|
/** @import { Material, MaterialOptions } from '../material/Material.js' */
|
|
13
13
|
/** @import { GeometryOptions } from '../geometry/Geometry.js' */
|
|
14
14
|
/** @import { NodeOptions } from './Node.js' */
|
|
15
|
-
/** @import { ColorInput, Vec3Input } from '../types.js' */
|
|
15
|
+
/** @import { ColorInput, QuatInput, Vec3Input } from '../types.js' */
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* @typedef {NodeOptions & {
|
|
@@ -45,11 +45,14 @@ import { NODE_OPTIONS, Node } from './Node.js'
|
|
|
45
45
|
* @property {'left' | 'right' | null} [edge] set the label at the view's left or right edge, level with its
|
|
46
46
|
* point - callouts in a column down the side of a diagram, with leader: true. offset then moves it in from
|
|
47
47
|
* the edge and up or down (default null: by its point)
|
|
48
|
+
* @property {number} [minScale] show the label only when zoomed in this far: at least this many CSS pixels to
|
|
49
|
+
* one world unit, at the label's point - details that appear as you come closer (default 0: always)
|
|
48
50
|
*/
|
|
49
51
|
/** @typedef {'center' | 'above' | 'below' | 'left' | 'right'} LabelPlace */
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
|
-
* One label per instance or point, from its row of data
|
|
54
|
+
* One label per instance or point, from its row of data - or a list of these, for several each (a symbol
|
|
55
|
+
* always, its details when zoomed in).
|
|
53
56
|
* @typedef {Omit<LabelPlacement, 'priority'> & {
|
|
54
57
|
* text: (datum: any, index: number) => string | number | null | undefined,
|
|
55
58
|
* priority?: number | ((datum: any, index: number) => number),
|
|
@@ -61,7 +64,7 @@ import { NODE_OPTIONS, Node } from './Node.js'
|
|
|
61
64
|
*/
|
|
62
65
|
|
|
63
66
|
export const LABEL_PLACES = /** @type {LabelPlace[]} */ (['center', 'above', 'below', 'left', 'right'])
|
|
64
|
-
const PLACEMENT_OPTIONS = ['place', 'offset', 'className', 'priority', 'overlap', 'occlude', 'leader', 'edge']
|
|
67
|
+
const PLACEMENT_OPTIONS = ['place', 'offset', 'className', 'priority', 'overlap', 'occlude', 'leader', 'edge', 'minScale']
|
|
65
68
|
const LABEL_HINTS = {
|
|
66
69
|
line: 'use leader: true - a line from the point to the label',
|
|
67
70
|
callout: "use leader: true, with edge: 'left' | 'right' to line the labels up down the side",
|
|
@@ -70,6 +73,8 @@ const LABEL_HINTS = {
|
|
|
70
73
|
pointer: 'use leader: true - a line from the point to the label',
|
|
71
74
|
anchor: "use place: 'center' | 'above' | 'below' | 'left' | 'right' (where the label goes)",
|
|
72
75
|
zIndex: "use 'priority': higher stays when labels overlap",
|
|
76
|
+
minZoom: "use 'minScale': CSS pixels to one world unit at the label's point",
|
|
77
|
+
lod: "use 'minScale': the label shows when zoomed in at least this far (CSS pixels to one world unit)",
|
|
73
78
|
collision: "use overlap: 'hide' | 'dim' | 'show'",
|
|
74
79
|
}
|
|
75
80
|
// labels per instance or point take one more: they can't be moved anywhere but by offset
|
|
@@ -94,12 +99,26 @@ function placement(owner, o) {
|
|
|
94
99
|
occlude: !!o.occlude,
|
|
95
100
|
leader: !!o.leader,
|
|
96
101
|
edge: o.edge == null ? null : /** @type {'left' | 'right'} */ (checkOneOf(owner, 'edge', o.edge, ['left', 'right'])),
|
|
102
|
+
minScale: o.minScale === undefined ? 0 : minScale(owner, o.minScale),
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
/** @param {string} owner @param {unknown} v @returns {
|
|
106
|
+
/** @param {string} owner @param {unknown} v @returns {number} */
|
|
107
|
+
function minScale(owner, v) {
|
|
108
|
+
const n = checkNumber(owner, 'minScale', v)
|
|
109
|
+
if (!(n >= 0)) fail(owner, `'minScale' is CSS pixels to one world unit, 0 or more, got ${n}`)
|
|
110
|
+
return n
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @typedef {InstanceLabels & Required<Omit<LabelPlacement, 'priority'>> & { priority: number | ((datum: any, index: number) => number) }} CheckedLabels */
|
|
114
|
+
|
|
115
|
+
/** @param {string} owner @param {unknown} v @returns {CheckedLabels | CheckedLabels[] | null} */
|
|
101
116
|
function instanceLabels(owner, v) {
|
|
102
117
|
if (v == null) return null
|
|
118
|
+
if (Array.isArray(v)) {
|
|
119
|
+
if (!v.length) fail(owner, "'labels' as a list needs at least one { text: (datum, index) => string, ... }")
|
|
120
|
+
return v.map((one) => /** @type {CheckedLabels} */ (instanceLabels(owner, one)))
|
|
121
|
+
}
|
|
103
122
|
if (typeof v !== 'object' || typeof (/** @type {any} */ (v)).text !== 'function') {
|
|
104
123
|
fail(owner, `'labels' must be { text: (datum, index) => string, ... }, got ${describe(v)}`)
|
|
105
124
|
}
|
|
@@ -253,7 +272,7 @@ export class Mesh extends Drawable {
|
|
|
253
272
|
* @typedef {object} InstanceOptions
|
|
254
273
|
* @property {Vec3Input} [position]
|
|
255
274
|
* @property {Vec3Input | Euler} [rotation] Euler angles in radians, XYZ order
|
|
256
|
-
* @property {
|
|
275
|
+
* @property {QuatInput} [quaternion] [x, y, z, w] or { x, y, z, w } - as physics engines give them
|
|
257
276
|
* @property {Vec3Input | number} [scale]
|
|
258
277
|
* @property {ColorInput} [color] multiplies the material colour
|
|
259
278
|
*/
|
|
@@ -261,11 +280,23 @@ export class Mesh extends Drawable {
|
|
|
261
280
|
const INSTANCE_OPTIONS = new Set(['position', 'rotation', 'quaternion', 'scale', 'color'])
|
|
262
281
|
// reused by setInstance, so updating every instance of a large mesh each frame allocates nothing
|
|
263
282
|
const scratch = { position: new Vec3(), scale: new Vec3(), quaternion: new Quat(), rotation: new Euler() }
|
|
283
|
+
const SCRATCH_COLOR = new Color()
|
|
264
284
|
const scratchMatrix = new Float32Array(16)
|
|
265
285
|
|
|
286
|
+
/**
|
|
287
|
+
* True when an instance is scaled to nothing - hidden: it draws nothing, takes no room in its mesh's
|
|
288
|
+
* bounds and shows no label.
|
|
289
|
+
* @internal @param {ArrayLike<number>} m instance matrices @param {number} i
|
|
290
|
+
*/
|
|
291
|
+
export function hiddenInstance(m, i) {
|
|
292
|
+
const o = i * 16
|
|
293
|
+
return !m[o] && !m[o + 1] && !m[o + 2] && !m[o + 4] && !m[o + 5] && !m[o + 6] && !m[o + 8] && !m[o + 9] && !m[o + 10]
|
|
294
|
+
}
|
|
295
|
+
|
|
266
296
|
/**
|
|
267
297
|
* Many copies of one geometry and material, drawn together. Set each copy with setInstance().
|
|
268
|
-
* Picking reports which instance was hit.
|
|
298
|
+
* Picking reports which instance was hit. An instance scaled to 0 is hidden - it draws nothing, and
|
|
299
|
+
* framing, culling, shadows and labels leave it out - so a pool of instances can keep spares.
|
|
269
300
|
*/
|
|
270
301
|
export class InstancedMesh extends Mesh {
|
|
271
302
|
static type = 'instanced-mesh'
|
|
@@ -279,12 +310,12 @@ export class InstancedMesh extends Mesh {
|
|
|
279
310
|
instanceColors = null
|
|
280
311
|
/** @internal Bumped when instance data changes, so renderers re-upload. */
|
|
281
312
|
version = 0
|
|
282
|
-
/** A label per instance
|
|
313
|
+
/** A label per instance from its row of data - or a list of them, several per instance. @type {ReturnType<typeof instanceLabels>} */
|
|
283
314
|
labels = null
|
|
284
315
|
|
|
285
316
|
/**
|
|
286
317
|
* @param {DrawableOptions & { count?: number, instances?: InstanceOptions[], instanceMatrices?: ArrayLike<number>,
|
|
287
|
-
* instanceColors?: ArrayLike<number>, labels?: InstanceLabels | null }} options count defaults to the number
|
|
318
|
+
* instanceColors?: ArrayLike<number>, labels?: InstanceLabels | InstanceLabels[] | null }} options count defaults to the number
|
|
288
319
|
* of instances or rows of data; instanceMatrices/instanceColors are raw data, as toJSON() writes
|
|
289
320
|
*/
|
|
290
321
|
constructor(options) {
|
|
@@ -375,7 +406,7 @@ export class InstancedMesh extends Mesh {
|
|
|
375
406
|
/** @param {number} i @param {ColorInput} color */
|
|
376
407
|
#setColor(i, color) {
|
|
377
408
|
if (!this.instanceColors) this.instanceColors = new Float32Array(this.instanceMatrices.length / 16 * 3).fill(1)
|
|
378
|
-
this.instanceColors.set(
|
|
409
|
+
this.instanceColors.set(SCRATCH_COLOR.set(color, 'InstancedMesh'), i * 3)
|
|
379
410
|
}
|
|
380
411
|
|
|
381
412
|
/** @internal @param {Box3} box */
|
|
@@ -401,6 +432,7 @@ export class InstancedMesh extends Mesh {
|
|
|
401
432
|
const ex = (local.max[0] - local.min[0]) / 2, ey = (local.max[1] - local.min[1]) / 2, ez = (local.max[2] - local.min[2]) / 2
|
|
402
433
|
const w = this.worldMatrix, im = this.instanceMatrices, m = scratchMatrix
|
|
403
434
|
for (let i = 0; i < this.count; i++) {
|
|
435
|
+
if (hiddenInstance(im, i)) continue
|
|
404
436
|
mat4.multiply(m, w, im.subarray(i * 16, i * 16 + 16))
|
|
405
437
|
const x = m[0] * cx + m[4] * cy + m[8] * cz + m[12]
|
|
406
438
|
const y = m[1] * cx + m[5] * cy + m[9] * cz + m[13]
|
|
@@ -436,10 +468,10 @@ export class Points extends Drawable {
|
|
|
436
468
|
static type = 'points'
|
|
437
469
|
static get OPTIONS() { return [...DRAWABLE_OPTIONS, 'labels'] }
|
|
438
470
|
|
|
439
|
-
/** A label per point
|
|
471
|
+
/** A label per point from its row of data - or a list of them, several per point. @type {ReturnType<typeof instanceLabels>} */
|
|
440
472
|
labels = null
|
|
441
473
|
|
|
442
|
-
/** @param {DrawableOptions & { labels?: InstanceLabels | null }} options */
|
|
474
|
+
/** @param {DrawableOptions & { labels?: InstanceLabels | InstanceLabels[] | null }} options */
|
|
443
475
|
constructor(options) {
|
|
444
476
|
super(options)
|
|
445
477
|
this.labels = instanceLabels(this.constructor.name, options?.labels)
|
|
@@ -505,6 +537,8 @@ export class Label extends Node {
|
|
|
505
537
|
leader = false
|
|
506
538
|
/** Set at the view's left or right edge, level with its point, or null for by its point. @type {'left' | 'right' | null} */
|
|
507
539
|
edge = null
|
|
540
|
+
/** Shown only when zoomed in this far: CSS pixels to one world unit at its point; 0 for always. */
|
|
541
|
+
minScale = 0
|
|
508
542
|
|
|
509
543
|
/** @param {LabelOptions} options */
|
|
510
544
|
constructor(options) {
|
|
@@ -529,7 +563,8 @@ export class Label extends Node {
|
|
|
529
563
|
/** @internal @returns {Record<string, unknown>} */
|
|
530
564
|
cloneOptions() {
|
|
531
565
|
return { ...super.cloneOptions(), text: this.text, className: this.className, place: this.place,
|
|
532
|
-
offset: [...this.offset], priority: this.priority, overlap: this.overlap, occlude: this.occlude, leader: this.leader, edge: this.edge
|
|
566
|
+
offset: [...this.offset], priority: this.priority, overlap: this.overlap, occlude: this.occlude, leader: this.leader, edge: this.edge,
|
|
567
|
+
minScale: this.minScale }
|
|
533
568
|
}
|
|
534
569
|
|
|
535
570
|
/** @internal @returns {Record<string, unknown>} */
|
|
@@ -544,6 +579,7 @@ export class Label extends Node {
|
|
|
544
579
|
if (this.occlude) json.occlude = true
|
|
545
580
|
if (this.leader) json.leader = true
|
|
546
581
|
if (this.edge) json.edge = this.edge
|
|
582
|
+
if (this.minScale) json.minScale = this.minScale
|
|
547
583
|
return json
|
|
548
584
|
}
|
|
549
585
|
}
|
package/src/scene/lights.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Color } from '../value/Color.js'
|
|
2
2
|
import { Vec3 } from '../value/Vec3.js'
|
|
3
|
-
import {
|
|
3
|
+
import { Box3 } from '../value/Box3.js'
|
|
4
|
+
import { checkNumber, describe, fail } from '../validate.js'
|
|
4
5
|
import { NODE_OPTIONS, Node, inParentSpace, setPoint } from './Node.js'
|
|
5
6
|
|
|
6
7
|
/** @import { NodeOptions } from './Node.js' */
|
|
7
|
-
/** @import { ColorInput, Vec3Input } from '../types.js' */
|
|
8
|
+
/** @import { Box3Input, ColorInput, Vec3Input } from '../types.js' */
|
|
8
9
|
|
|
9
10
|
/** @typedef {NodeOptions & { color?: ColorInput, intensity?: number }} LightOptions */
|
|
10
11
|
/** @typedef {'ambient' | 'hemisphere' | 'directional' | 'point' | 'spot'} LightKind */
|
|
@@ -189,24 +190,57 @@ export class DirectionalLight extends AimedLight {
|
|
|
189
190
|
static type = 'directional-light'
|
|
190
191
|
/** @internal @type {LightKind} */
|
|
191
192
|
static kind = 'directional'
|
|
192
|
-
static get OPTIONS() { return [...LIGHT_OPTIONS, 'direction', 'target', 'castShadow'] }
|
|
193
|
+
static get OPTIONS() { return [...LIGHT_OPTIONS, 'direction', 'target', 'castShadow', 'shadowBounds'] }
|
|
193
194
|
|
|
194
195
|
castShadow = false
|
|
196
|
+
/** @type {Box3 | null} */
|
|
197
|
+
#shadowBounds = null
|
|
195
198
|
|
|
196
|
-
/** @param {AimedLightOptions & { castShadow?: boolean }} [options] */
|
|
199
|
+
/** @param {AimedLightOptions & { castShadow?: boolean, shadowBounds?: Box3Input | null }} [options] */
|
|
197
200
|
constructor(options = {}) {
|
|
198
201
|
super(options)
|
|
199
202
|
if (options.castShadow !== undefined) this.castShadow = !!options.castShadow
|
|
203
|
+
if (options.shadowBounds !== undefined) this.#setShadowBounds(options.shadowBounds)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The part of the world, in world units, that has shadows - or null (the default) to follow
|
|
208
|
+
* whatever casts them. The shadow map is fitted to what casts every frame, so one caster flung far
|
|
209
|
+
* off spreads its texels over everything and every shadow blurs; set the area where things happen
|
|
210
|
+
* and its shadows stay sharp. Casters outside it still shadow it, from wherever they stand.
|
|
211
|
+
* Assigning copies.
|
|
212
|
+
* @returns {Box3 | null}
|
|
213
|
+
*/
|
|
214
|
+
get shadowBounds() { return this.#shadowBounds }
|
|
215
|
+
|
|
216
|
+
/** @param {Box3Input | null} v */
|
|
217
|
+
set shadowBounds(v) { this.#setShadowBounds(v) }
|
|
218
|
+
|
|
219
|
+
/** @param {Box3Input | null} v */
|
|
220
|
+
#setShadowBounds(v) {
|
|
221
|
+
if (v === null) {
|
|
222
|
+
this.#shadowBounds = null
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
const b = /** @type {any} */ (v)
|
|
226
|
+
if (!b || typeof b !== 'object' || !b.min || !b.max) {
|
|
227
|
+
fail(this.constructor.name, `'shadowBounds' is a box - { min: [x, y, z], max: [x, y, z] } or a Box3 - or null, got ${describe(v)}`)
|
|
228
|
+
}
|
|
229
|
+
const box = new Box3(Vec3.fromInput(b.min, this.constructor.name), Vec3.fromInput(b.max, this.constructor.name))
|
|
230
|
+
if (box.isEmpty()) fail(this.constructor.name, `'shadowBounds' min must not be above max on any axis, got ${describe(v)}`)
|
|
231
|
+
this.#shadowBounds = box
|
|
200
232
|
}
|
|
201
233
|
|
|
202
234
|
/** @internal @returns {Record<string, unknown>} */
|
|
203
235
|
cloneOptions() {
|
|
204
|
-
return { ...super.cloneOptions(), castShadow: this.castShadow }
|
|
236
|
+
return { ...super.cloneOptions(), castShadow: this.castShadow, shadowBounds: this.#shadowBounds }
|
|
205
237
|
}
|
|
206
238
|
|
|
207
239
|
/** @internal @returns {Record<string, unknown>} */
|
|
208
240
|
jsonOptions() {
|
|
209
|
-
|
|
241
|
+
const b = this.#shadowBounds
|
|
242
|
+
return { ...super.jsonOptions(), ...(this.castShadow ? { castShadow: true } : {}),
|
|
243
|
+
...(b ? { shadowBounds: { min: Array.from(b.min), max: Array.from(b.max) } } : {}) }
|
|
210
244
|
}
|
|
211
245
|
}
|
|
212
246
|
|
package/src/shadow.js
CHANGED
|
@@ -23,9 +23,10 @@ export const SHADOW_MAP_SIZE = 2048
|
|
|
23
23
|
* @param {Box3[]} casters world bounds
|
|
24
24
|
* @param {Box3[]} receivers world bounds
|
|
25
25
|
* @param {number} [size] map size in texels
|
|
26
|
+
* @param {Box3 | null} [area] the light's shadowBounds: the map covers this across, whatever casts
|
|
26
27
|
* @returns {ShadowFit | null}
|
|
27
28
|
*/
|
|
28
|
-
export function fitShadow(direction, casters, receivers, size = SHADOW_MAP_SIZE) {
|
|
29
|
+
export function fitShadow(direction, casters, receivers, size = SHADOW_MAP_SIZE, area = null) {
|
|
29
30
|
const boxes = casters.filter((b) => !b.isEmpty())
|
|
30
31
|
if (!boxes.length) return null
|
|
31
32
|
const d = normalized(direction)
|
|
@@ -50,7 +51,10 @@ export function fitShadow(direction, casters, receivers, size = SHADOW_MAP_SIZE)
|
|
|
50
51
|
if (z > z1) z1 = z
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
// across the light's view: the area it was given, or else the casters; in depth, everything that
|
|
55
|
+
// casts - so a caster outside the area, between it and the light, still shadows it
|
|
56
|
+
if (area) corners(area, true)
|
|
57
|
+
for (const b of boxes) corners(b, !area)
|
|
54
58
|
// receivers only stretch the depth (view z is negative in front, so the far end is the minimum)
|
|
55
59
|
for (const b of receivers) if (!b.isEmpty()) corners(b, false)
|
|
56
60
|
|
|
@@ -58,10 +62,29 @@ export function fitShadow(direction, casters, receivers, size = SHADOW_MAP_SIZE)
|
|
|
58
62
|
const margin = Math.max(x1 - x0, y1 - y0, 1e-3) * 0.02
|
|
59
63
|
x0 -= margin; x1 += margin; y0 -= margin; y1 += margin
|
|
60
64
|
const depthMargin = Math.max(z1 - z0, 1e-3) * 0.01
|
|
61
|
-
|
|
65
|
+
z0 -= depthMargin; z1 += depthMargin
|
|
66
|
+
|
|
67
|
+
// steady while things move: the map grows and shrinks in small steps rather than with every change
|
|
68
|
+
// in the casters' extent, and its edges sit on the texel grid, so a shadow that isn't moving falls
|
|
69
|
+
// on the same texels every frame, instead of shimmering as the fit slides about under it
|
|
70
|
+
const w = steps(x1 - x0), h = steps(y1 - y0)
|
|
71
|
+
const tx = w / size, ty = h / size
|
|
72
|
+
x0 = Math.floor((x0 + x1 - w) / 2 / tx) * tx
|
|
73
|
+
y0 = Math.floor((y0 + y1 - h) / 2 / ty) * ty
|
|
74
|
+
// the depth range too, which sets what each stored depth means
|
|
75
|
+
const dz = steps(z1 - z0) / 64
|
|
76
|
+
z0 = Math.floor(z0 / dz) * dz
|
|
77
|
+
z1 = Math.ceil(z1 / dz) * dz
|
|
78
|
+
|
|
79
|
+
const projection = mat4.ortho(mat4.create(), x0, x0 + w, y0, y0 + h, -z1, -z0)
|
|
62
80
|
const viewProjection = new Mat4()
|
|
63
81
|
mat4.multiply(viewProjection, projection, view)
|
|
64
|
-
return { viewProjection, texelSize: Math.max(
|
|
82
|
+
return { viewProjection, texelSize: Math.max(w, h) / size }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A length rounded up to the next of a ladder of sizes, sixteen to each doubling. @param {number} v */
|
|
86
|
+
function steps(v) {
|
|
87
|
+
return 2 ** (Math.ceil(Math.log2(Math.max(v, 1e-3)) * 16) / 16)
|
|
65
88
|
}
|
|
66
89
|
|
|
67
90
|
/** Spot shadows' map: four tiles of half its size, one a light. */
|
package/src/types.js
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
|
|
10
10
|
/** A vector as accepted on input: [x, y, z], { x, y, z } or a Vec3. @typedef {Vec3Like | XYZ} Vec3Input */
|
|
11
11
|
|
|
12
|
+
/** A quaternion as an object literal - as physics engines such as Rapier return them. @typedef {{ x: number, y: number, z: number, w: number }} XYZW */
|
|
13
|
+
|
|
14
|
+
/** A rotation quaternion as accepted on input: [x, y, z, w], { x, y, z, w } or a Quat. @typedef {ArrayLike<number> | XYZW} QuatInput */
|
|
15
|
+
|
|
16
|
+
/** A box as accepted on input: a Box3, or its lowest and highest corners. @typedef {import('./value/Box3.js').Box3 | { min: Vec3Input, max: Vec3Input }} Box3Input */
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
19
|
* A colour as accepted on input, in sRGB: '#ff8800', '#f80', 0xff8800, 'rgb(255 136 0)',
|
|
14
20
|
* 'hsl(32 100% 50%)', a CSS colour name, or [r, g, b] with components 0-1.
|
package/src/value/Color.js
CHANGED
|
@@ -16,6 +16,11 @@ const HELP = "use '#rrggbb', a CSS name, 'rgb(...)', 'hsl(...)', 0xrrggbb or [r,
|
|
|
16
16
|
* An RGB colour. Input is sRGB, as everywhere in phoria2; the components are stored **linear**,
|
|
17
17
|
* ready for lighting maths. `toJSON()` gives sRGB '#rrggbb' back.
|
|
18
18
|
*/
|
|
19
|
+
// colour strings already worked out, as linear rgb - emptied when full, as a palette never fills it
|
|
20
|
+
/** @type {Map<string, number[]>} */
|
|
21
|
+
const PARSED = new Map()
|
|
22
|
+
const PARSED_MAX = 1024
|
|
23
|
+
|
|
19
24
|
export class Color extends Float32Array {
|
|
20
25
|
static get [Symbol.species]() { return Float32Array }
|
|
21
26
|
|
|
@@ -154,6 +159,22 @@ export class Color extends Float32Array {
|
|
|
154
159
|
|
|
155
160
|
/** @param {string} s @param {string} owner */
|
|
156
161
|
#setString(s, owner) {
|
|
162
|
+
// the same few strings come again and again - an instance's colour set every frame - so each is
|
|
163
|
+
// worked out once
|
|
164
|
+
const known = PARSED.get(s)
|
|
165
|
+
if (known) {
|
|
166
|
+
this[0] = known[0]
|
|
167
|
+
this[1] = known[1]
|
|
168
|
+
this[2] = known[2]
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
this.#parse(s, owner)
|
|
172
|
+
if (PARSED.size >= PARSED_MAX) PARSED.clear()
|
|
173
|
+
PARSED.set(s, [this[0], this[1], this[2]])
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @param {string} s @param {string} owner */
|
|
177
|
+
#parse(s, owner) {
|
|
157
178
|
const lower = s.toLowerCase()
|
|
158
179
|
if (lower.startsWith('#')) {
|
|
159
180
|
let hex = lower.slice(1)
|
package/src/value/Quat.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as quat from '../math/quat.js'
|
|
2
2
|
import { describe, fail } from '../validate.js'
|
|
3
3
|
|
|
4
|
+
/** @import { QuatInput } from '../types.js' */
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* A rotation quaternion [x, y, z, w]: a Float32Array(4) with accessors and chainable methods.
|
|
6
8
|
* A node's quaternion and its Euler `rotation` keep each other in sync through these setters;
|
|
@@ -61,13 +63,20 @@ export class Quat extends Float32Array {
|
|
|
61
63
|
return this.changed()
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
/** @param {
|
|
66
|
+
/** @param {QuatInput} q [x, y, z, w], or { x, y, z, w } as physics engines give them @param {string} [owner] @returns {this} */
|
|
65
67
|
copy(q, owner = 'Quat') {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const o = /** @type {any} */ (q)
|
|
69
|
+
if (o && typeof o === 'object' && o.length >= 4) {
|
|
70
|
+
this[0] = o[0]
|
|
71
|
+
this[1] = o[1]
|
|
72
|
+
this[2] = o[2]
|
|
73
|
+
this[3] = o[3]
|
|
74
|
+
} else if (o && typeof o === 'object' && 'x' in o && 'y' in o && 'z' in o && 'w' in o) {
|
|
75
|
+
this[0] = o.x
|
|
76
|
+
this[1] = o.y
|
|
77
|
+
this[2] = o.z
|
|
78
|
+
this[3] = o.w
|
|
79
|
+
} else fail(owner, `expected a quaternion [x, y, z, w] or { x, y, z, w }, got ${describe(q)}`)
|
|
71
80
|
return this.changed()
|
|
72
81
|
}
|
|
73
82
|
|
package/src/watch.js
CHANGED
|
@@ -123,7 +123,7 @@ export class ChangeWatch {
|
|
|
123
123
|
this.#push((node.handlersFor('click').length ? 1 : 0) | (node.handlersFor('hover').length ? 2 : 0) | (node.tooltip ? 4 : 0) | (node.onDrag ? 8 : 0) | (node.highlightWhole ? 16 : 0))
|
|
124
124
|
// what hovering tints it with - a different colour shows straight away on what is hovered
|
|
125
125
|
if (node.highlight) this.#pushAll(node.highlight)
|
|
126
|
-
else this.#push(-4)
|
|
126
|
+
else this.#push(node.highlight === false ? -5 : -4)
|
|
127
127
|
const outline = node.outline
|
|
128
128
|
if (outline) {
|
|
129
129
|
this.#pushAll(outline.color)
|
|
@@ -169,6 +169,22 @@ export class ChangeWatch {
|
|
|
169
169
|
this.#push(m.aoMapIntensity)
|
|
170
170
|
this.#push(m.triplanar)
|
|
171
171
|
this.#push(m.bumpScale)
|
|
172
|
+
this.#push(m.transmission)
|
|
173
|
+
this.#push(m.clearcoat)
|
|
174
|
+
this.#push(m.clearcoatRoughness)
|
|
175
|
+
this.#push(m.sheen)
|
|
176
|
+
this.#pushAll(m.sheenColor)
|
|
177
|
+
this.#push(m.sheenRoughness)
|
|
178
|
+
this.#push(m.ior)
|
|
179
|
+
this.#push(m.specularIntensity)
|
|
180
|
+
this.#pushAll(m.specularColor)
|
|
181
|
+
this.#push(m.anisotropy)
|
|
182
|
+
this.#push(m.anisotropyRotation)
|
|
183
|
+
this.#push(m.translucency)
|
|
184
|
+
this.#push(m.toon)
|
|
185
|
+
this.#push(m.lightMapIntensity)
|
|
186
|
+
this.#push(m.environmentIntensity)
|
|
187
|
+
this.#pushAll(m.translucencyColor)
|
|
172
188
|
this.#push((m.unlit ? 1 : 0) | (m.flatShading ? 2 : 0) | (m.doubleSided ? 4 : 0) | (m.vertexColors ? 8 : 0) |
|
|
173
189
|
(m.depthWrite ? 16 : 0) | (m.depthTest ? 32 : 0) | (m.sizeAttenuation ? 64 : 0) | (m.blending === 'additive' ? 128 : m.blending === 'multiply' ? 1024 : 0) | (m.transparent ? 256 : 0) | (m.fog ? 512 : 0))
|
|
174
190
|
for (const [, texture] of m.textures) this.#texture(texture)
|
|
@@ -179,6 +195,9 @@ export class ChangeWatch {
|
|
|
179
195
|
this.#push(idOf(texture))
|
|
180
196
|
this.#push(texture.version)
|
|
181
197
|
this.#pushAll(texture.repeat)
|
|
198
|
+
this.#push(texture.rotation)
|
|
199
|
+
this.#pushAll(texture.center)
|
|
200
|
+
this.#push(texture.uvSet)
|
|
182
201
|
this.#pushAll(texture.offset)
|
|
183
202
|
}
|
|
184
203
|
|
|
@@ -189,6 +208,7 @@ export class ChangeWatch {
|
|
|
189
208
|
this.#push(label.priority)
|
|
190
209
|
this.#push(label.occlude ? 1 : 0)
|
|
191
210
|
this.#push(label.leader ? 1 : 0)
|
|
211
|
+
this.#push(label.minScale)
|
|
192
212
|
}
|
|
193
213
|
|
|
194
214
|
/** @param {any} light */
|
|
@@ -199,6 +219,10 @@ export class ChangeWatch {
|
|
|
199
219
|
if (light.direction) this.#pushAll(light.direction)
|
|
200
220
|
if (light.target) this.#pushAll(light.target)
|
|
201
221
|
if (light.castShadow) this.#push(1)
|
|
222
|
+
if (light.shadowBounds) {
|
|
223
|
+
this.#pushAll(light.shadowBounds.min)
|
|
224
|
+
this.#pushAll(light.shadowBounds.max)
|
|
225
|
+
}
|
|
202
226
|
if ('range' in light) this.#push(light.range)
|
|
203
227
|
if ('angle' in light) {
|
|
204
228
|
this.#push(light.angle)
|
|
@@ -6,6 +6,7 @@ import { CLIP_ONLY_FRAGMENT, MESH_VERTEX } from './shaders.js'
|
|
|
6
6
|
import { collect } from '../frame.js'
|
|
7
7
|
import { checkNumber, checkOneOf, checkOptions, checkRange, describe, fail, warnOnce } from '../validate.js'
|
|
8
8
|
import { GLContext } from './context.js'
|
|
9
|
+
import { uvMatrix } from './resources.js'
|
|
9
10
|
|
|
10
11
|
/** @import { FrameDescription, DrawItem, FrameLight, FrameCap } from '../frame.js' */
|
|
11
12
|
/** @import { Scene } from '../scene/Scene.js' */
|
|
@@ -32,6 +33,7 @@ import { GLContext } from './context.js'
|
|
|
32
33
|
* this canvas, instead of giving the canvas a context of its own - for pages with many views
|
|
33
34
|
* (browsers allow about 16 live contexts). The View decides this; see its 'context' option.
|
|
34
35
|
* @property {boolean} [antialias] multisampling (default true; a shared context always has it)
|
|
36
|
+
* @property {boolean} [outlineAntialias] soft-edged outlines with round corners (default: as antialias)
|
|
35
37
|
* @property {ToneMapping} [toneMapping] how lit colours are mapped to the screen: 'neutral' keeps colours
|
|
36
38
|
* true (Khronos PBR Neutral, the default), 'aces' is filmic, 'none' clips
|
|
37
39
|
* @property {number} [exposure] (default 1)
|
|
@@ -39,7 +41,7 @@ import { GLContext } from './context.js'
|
|
|
39
41
|
* @property {boolean | BloomOptions} [bloom] glow round bright parts (default off); loads its code the first time
|
|
40
42
|
*/
|
|
41
43
|
|
|
42
|
-
const OPTIONS = ['canvas', 'shared', 'antialias', 'toneMapping', 'exposure', 'pixelRatio', 'bloom']
|
|
44
|
+
const OPTIONS = ['canvas', 'shared', 'antialias', 'outlineAntialias', 'toneMapping', 'exposure', 'pixelRatio', 'bloom']
|
|
43
45
|
const BLOOM = { strength: 1, radius: 0.6, threshold: 0.6 }
|
|
44
46
|
/** What bloom: { ... } takes, and hints for names people reach for instead. */
|
|
45
47
|
export const BLOOM_OPTIONS = Object.keys(BLOOM)
|
|
@@ -66,10 +68,10 @@ const TONE_MAPPING = { none: 0, aces: 1, neutral: 2 }
|
|
|
66
68
|
// material maps and the shader feature each one switches on (all but emissive need lighting)
|
|
67
69
|
/** @type {import('./passes.js').ShadowMap[]} */
|
|
68
70
|
const NO_SHADOWS = []
|
|
69
|
-
const MAP_DEFINES = { roughnessMap: 'ROUGHNESS_MAP', metalnessMap: 'METALNESS_MAP', normalMap: 'NORMAL_MAP', bumpMap: 'BUMP_MAP', aoMap: 'AO_MAP' }
|
|
71
|
+
const MAP_DEFINES = { roughnessMap: 'ROUGHNESS_MAP', metalnessMap: 'METALNESS_MAP', normalMap: 'NORMAL_MAP', bumpMap: 'BUMP_MAP', aoMap: 'AO_MAP', lightMap: 'LIGHT_MAP' }
|
|
70
72
|
// every map a mesh can sample - triplanar projection is only worth its varyings when there is one
|
|
71
73
|
const MAP_SLOTS = ['map', 'emissiveMap', ...Object.keys(MAP_DEFINES)]
|
|
72
|
-
const MAP_UNIFORMS = { map: 'uMap', roughnessMap: 'uRoughnessMap', metalnessMap: 'uMetalnessMap', normalMap: 'uNormalMap', bumpMap: 'uBumpMap', aoMap: 'uAoMap', emissiveMap: 'uEmissiveMap' }
|
|
74
|
+
const MAP_UNIFORMS = { matcap: 'uMatcap', map: 'uMap', roughnessMap: 'uRoughnessMap', metalnessMap: 'uMetalnessMap', normalMap: 'uNormalMap', bumpMap: 'uBumpMap', aoMap: 'uAoMap', emissiveMap: 'uEmissiveMap', lightMap: 'uLightMap' }
|
|
73
75
|
// counting a cap's faces into the stencil: the solids' surfaces, colour and depth left alone
|
|
74
76
|
const CLIP_MASK = /** @type {[string, string]} */ ([MESH_VERTEX, CLIP_ONLY_FRAGMENT])
|
|
75
77
|
// a cap: a square on the plane, two units across, facing +Y until it is placed
|
|
@@ -96,6 +98,8 @@ export class WebGLRenderer {
|
|
|
96
98
|
/** @type {ToneMapping} */ toneMapping = 'neutral'
|
|
97
99
|
exposure = 1
|
|
98
100
|
pixelRatio = 1
|
|
101
|
+
/** Outlines with soft edges and round corners, or hard pixel steps; takes effect on the next frame. */
|
|
102
|
+
outlineAntialias = true
|
|
99
103
|
/** @type {Required<BloomOptions> | null} */ #bloom = null
|
|
100
104
|
/** Counts from the last frame and the GPU caches. */
|
|
101
105
|
info = { drawCalls: 0, triangles: 0, programs: 0, geometries: 0, textures: 0, compiling: 0 }
|
|
@@ -129,6 +133,8 @@ export class WebGLRenderer {
|
|
|
129
133
|
if (options.exposure !== undefined) this.exposure = checkRange('WebGLRenderer', 'exposure', options.exposure, 0)
|
|
130
134
|
if (options.pixelRatio !== undefined) this.pixelRatio = checkRange('WebGLRenderer', 'pixelRatio', options.pixelRatio, 0, Infinity, { above: true })
|
|
131
135
|
this.#bloom = bloomSettings('WebGLRenderer', options.bloom)
|
|
136
|
+
// outlines follow the picture's antialiasing unless told otherwise (a shared context always has it)
|
|
137
|
+
this.outlineAntialias = options.outlineAntialias !== undefined ? !!options.outlineAntialias : options.shared || options.antialias !== false
|
|
132
138
|
|
|
133
139
|
if (options.shared) {
|
|
134
140
|
this.#gpu = GLContext.shared()
|
|
@@ -230,7 +236,7 @@ export class WebGLRenderer {
|
|
|
230
236
|
frame.pointShadow && passes.pointShadowMap(frame.pointShadow),
|
|
231
237
|
].filter((s) => !!s)
|
|
232
238
|
// outlines' mask, drawn before the scene, so the scene's own target need not be left
|
|
233
|
-
const outline = passes && frame.outlines.length ? passes.outlineMask(frame.outlines, frame, width, height, this.pixelRatio) : null
|
|
239
|
+
const outline = passes && frame.outlines.length ? passes.outlineMask(frame.outlines, frame, width, height, this.pixelRatio, this.outlineAntialias) : null
|
|
234
240
|
|
|
235
241
|
// with bloom, the scene is drawn into a buffer of its own first (drawn plainly while its code loads)
|
|
236
242
|
const bloom = this.#bloom ? gpu.loadBloom() : null
|
|
@@ -268,7 +274,7 @@ export class WebGLRenderer {
|
|
|
268
274
|
gl.enable(gl.BLEND)
|
|
269
275
|
for (const item of frame.transparent) this.#draw(item, frame, true)
|
|
270
276
|
// outlines over everything
|
|
271
|
-
if (outline && passes) passes.outlineComposite(outline
|
|
277
|
+
if (outline && passes) passes.outlineComposite(outline)
|
|
272
278
|
gl.depthMask(true)
|
|
273
279
|
gl.disable(gl.BLEND)
|
|
274
280
|
gl.bindVertexArray(null)
|
|
@@ -400,7 +406,7 @@ export class WebGLRenderer {
|
|
|
400
406
|
const shadows = receives ? this.#shadows : NO_SHADOWS
|
|
401
407
|
|
|
402
408
|
const clip = kind === 'mesh' ? item.clip : null
|
|
403
|
-
const defines = this.#defines(kind, material, geometry.colors !== null, !!instances)
|
|
409
|
+
const defines = this.#defines(kind, material, geometry.colors !== null, !!instances, geometry.uvs2 !== null)
|
|
404
410
|
for (const shadow of shadows) defines.push(shadow.define)
|
|
405
411
|
const fog = material.fog ? frame.fog : null
|
|
406
412
|
if (fog) {
|
|
@@ -477,15 +483,38 @@ export class WebGLRenderer {
|
|
|
477
483
|
if (material.bumpMap) program.set('uBumpScale', material.bumpScale)
|
|
478
484
|
if (material.triplanar > 0) program.set('uTriplanar', material.triplanar)
|
|
479
485
|
program.set('uAoMapIntensity', material.aoMapIntensity)
|
|
486
|
+
if (material.transmission > 0) program.set('uTransmission', material.transmission)
|
|
487
|
+
if (material.clearcoat > 0) {
|
|
488
|
+
program.set('uClearcoat', material.clearcoat)
|
|
489
|
+
program.set('uClearcoatRoughness', material.clearcoatRoughness)
|
|
490
|
+
}
|
|
491
|
+
// a non-metal's reflectance face on, from its ior and specular settings, and at grazing angles
|
|
492
|
+
const ior = (material.ior - 1) / (material.ior + 1), sc = material.specularColor, si = material.specularIntensity
|
|
493
|
+
program.set('uSpecular', [Math.min(ior * ior * sc[0], 1) * si, Math.min(ior * ior * sc[1], 1) * si, Math.min(ior * ior * sc[2], 1) * si, si])
|
|
494
|
+
if (material.sheen > 0) {
|
|
495
|
+
const c = material.sheenColor, k = material.sheen
|
|
496
|
+
program.set('uSheenColor', [c[0] * k, c[1] * k, c[2] * k])
|
|
497
|
+
program.set('uSheenRoughness', material.sheenRoughness)
|
|
498
|
+
}
|
|
499
|
+
if (material.anisotropy > 0) {
|
|
500
|
+
program.set('uAnisotropy', material.anisotropy)
|
|
501
|
+
program.set('uAnisotropyDirection', [Math.cos(material.anisotropyRotation), Math.sin(material.anisotropyRotation)])
|
|
502
|
+
}
|
|
503
|
+
if (material.translucency > 0) {
|
|
504
|
+
const c = material.translucencyColor, k = material.translucency
|
|
505
|
+
program.set('uTranslucency', [c[0] * k, c[1] * k, c[2] * k, k])
|
|
506
|
+
}
|
|
507
|
+
if (material.toon > 0) program.set('uToon', Math.max(2, Math.round(material.toon)))
|
|
480
508
|
program.set('uAlphaTest', material.alphaTest)
|
|
481
509
|
/** @type {[string, Texture][]} */
|
|
482
510
|
const maps = kind === 'mesh' ? material.textures : material.map ? [['map', material.map]] : []
|
|
483
511
|
// GL textures first (uploads use a unit of their own), then bind them to the samplers
|
|
484
512
|
const bound = maps.map(([slot, texture]) => [MAP_UNIFORMS[slot], this.#gpu.resources.texture(texture)])
|
|
485
513
|
for (const [name, texture] of bound) program.set(name, texture)
|
|
486
|
-
//
|
|
487
|
-
const
|
|
488
|
-
program.set('
|
|
514
|
+
// each map's own repeat, rotation and offset (a matcap takes no UVs)
|
|
515
|
+
for (const [slot, texture] of maps) if (slot !== 'matcap') program.set(`${MAP_UNIFORMS[slot]}Uv`, uvMatrix(texture))
|
|
516
|
+
if (material.lightMap) program.set('uLightMapIntensity', material.lightMapIntensity)
|
|
517
|
+
program.set('uEnvStrength', material.environmentIntensity)
|
|
489
518
|
|
|
490
519
|
// render state
|
|
491
520
|
gl.depthMask(!transparent && material.depthWrite)
|
|
@@ -535,7 +564,7 @@ export class WebGLRenderer {
|
|
|
535
564
|
// drawing's outline - shows whole rather than fighting it pixel by pixel
|
|
536
565
|
gl.enable(gl.POLYGON_OFFSET_FILL)
|
|
537
566
|
gl.polygonOffset(1, 2)
|
|
538
|
-
this.info.triangles += this.#gpu.resources.drawTriangles(geometry, instances)
|
|
567
|
+
this.info.triangles += this.#gpu.resources.drawTriangles(geometry, instances, material.anisotropy > 0 && !material.unlit)
|
|
539
568
|
gl.disable(gl.POLYGON_OFFSET_FILL)
|
|
540
569
|
this.info.drawCalls++
|
|
541
570
|
}
|
|
@@ -613,10 +642,10 @@ export class WebGLRenderer {
|
|
|
613
642
|
|
|
614
643
|
/**
|
|
615
644
|
* @param {'mesh' | 'points' | 'lines'} kind @param {Material} material
|
|
616
|
-
* @param {boolean} hasColors @param {boolean} instanced
|
|
645
|
+
* @param {boolean} hasColors @param {boolean} instanced @param {boolean} hasUvs2
|
|
617
646
|
* @returns {string[]}
|
|
618
647
|
*/
|
|
619
|
-
#defines(kind, material, hasColors, instanced) {
|
|
648
|
+
#defines(kind, material, hasColors, instanced, hasUvs2) {
|
|
620
649
|
const d = []
|
|
621
650
|
if (material.vertexColors && hasColors) d.push('VERTEX_COLORS')
|
|
622
651
|
if (kind === 'mesh') {
|
|
@@ -626,6 +655,13 @@ export class WebGLRenderer {
|
|
|
626
655
|
d.push(...this.#lightDefines)
|
|
627
656
|
if (this.#environment) d.push('ENV_MAP')
|
|
628
657
|
else if (this.#studio) d.push('STUDIO')
|
|
658
|
+
if (material.transmission > 0) d.push('TRANSMISSION')
|
|
659
|
+
if (material.clearcoat > 0) d.push('CLEARCOAT')
|
|
660
|
+
if (material.sheen > 0) d.push('SHEEN')
|
|
661
|
+
if (material.anisotropy > 0) d.push('ANISOTROPY')
|
|
662
|
+
if (material.translucency > 0) d.push('TRANSLUCENCY')
|
|
663
|
+
if (material.matcap) d.push('MATCAP')
|
|
664
|
+
if (material.toon > 0) d.push('TOON')
|
|
629
665
|
}
|
|
630
666
|
if (material.map) d.push('MAP')
|
|
631
667
|
if (material.emissiveMap) d.push('EMISSIVE_MAP')
|
|
@@ -633,6 +669,9 @@ export class WebGLRenderer {
|
|
|
633
669
|
if (material.flatShading) d.push('FLAT_SHADING')
|
|
634
670
|
if (material.doubleSided) d.push('DOUBLE_SIDED')
|
|
635
671
|
if (material.triplanar > 0 && MAP_SLOTS.some((slot) => material[slot])) d.push('TRIPLANAR')
|
|
672
|
+
// maps on the second UV set read it - where the geometry has one
|
|
673
|
+
const second = hasUvs2 ? material.textures.filter(([slot, t]) => t.uvSet === 1 && slot !== 'matcap') : []
|
|
674
|
+
if (second.length) d.push('UV2', ...second.map(([slot]) => `${MAP_UNIFORMS[slot]}Set vUv2`))
|
|
636
675
|
if (instanced) d.push('INSTANCED')
|
|
637
676
|
} else if (kind === 'points') {
|
|
638
677
|
if (material.map) d.push('MAP')
|