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.
- package/README.md +6 -6
- package/dist/bloom.js +4 -4
- package/dist/bloom.js.map +1 -1
- package/dist/charts.js +377 -377
- package/dist/charts.js.map +1 -1
- package/dist/core.js +1052 -558
- package/dist/core.js.map +1 -1
- package/dist/edit.js +48 -48
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +1 -1
- package/dist/gltf.js +207 -162
- package/dist/gltf.js.map +1 -1
- package/dist/motion.js +27 -27
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +50 -50
- package/dist/particles.js.map +1 -1
- package/dist/passes.js +199 -97
- package/dist/passes.js.map +1 -1
- package/dist/phoria2.js +1 -1
- package/dist/picking.js +16 -21
- 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/View.d.ts +5 -0
- package/dist/types/geometry/Geometry.d.ts +6 -0
- package/dist/types/material/Material.d.ts +125 -0
- package/dist/types/material/Texture.d.ts +37 -5
- 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 +96 -7
- package/package.json +1 -1
- package/schema/scene.schema.json +127 -0
- package/src/View.js +4 -2
- package/src/geometry/Geometry.js +8 -1
- package/src/gltf/load.js +50 -6
- package/src/material/Material.js +104 -19
- package/src/material/Texture.js +116 -11
- package/src/watch.js +19 -0
- 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/webgl/passes.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as mat4 from '../math/mat4.js'
|
|
|
5
5
|
import { irradianceSH } from '../irradiance.js'
|
|
6
6
|
import { POINT_SHADOW_SIZE, SHADOW_MAP_SIZE, SPOT_ATLAS_SIZE } from '../shadow.js'
|
|
7
7
|
import { CLIP, CLIP_ONLY_FRAGMENT, COMMON, EQUIRECT, FULLSCREEN_VERTEX, MESH_VERTEX, TONE_MAP } from './shaders.js'
|
|
8
|
+
import { uvMatrix } from './resources.js'
|
|
8
9
|
|
|
9
10
|
/** @import { Programs } from './programs.js' */
|
|
10
11
|
/** @import { Resources } from './resources.js' */
|
|
@@ -154,9 +155,98 @@ void main() {
|
|
|
154
155
|
fragColor = vec4(linearToSrgb(ring.rgb), 1.0);
|
|
155
156
|
}
|
|
156
157
|
`
|
|
158
|
+
// Antialiased outlines. The mask is drawn multisampled, with a second target saying how much of
|
|
159
|
+
// each pixel it covers; once resolved, the mask holds colour and width times that coverage. The
|
|
160
|
+
// ring is then measured from the shape's edge, not from masked pixels' middles - a pixel's edge is
|
|
161
|
+
// its coverage less a half beyond its middle - and in true distance, so corners come out round. Its
|
|
162
|
+
// outer edge fades over a pixel, and its inner edge leaves the shape's share of each pixel alone.
|
|
163
|
+
const OUTLINE_COVER_FRAGMENT = /* glsl */ `
|
|
164
|
+
precision highp float;
|
|
165
|
+
in vec3 vWorldPosition;
|
|
166
|
+
uniform vec4 uMask;
|
|
167
|
+
layout(location = 0) out vec4 fragColor;
|
|
168
|
+
layout(location = 1) out vec4 fragCoverage;
|
|
169
|
+
${CLIP}
|
|
170
|
+
void main() {
|
|
171
|
+
clipAway(vWorldPosition);
|
|
172
|
+
fragColor = uMask;
|
|
173
|
+
fragCoverage = vec4(1.0);
|
|
174
|
+
}
|
|
175
|
+
`
|
|
176
|
+
const SMOOTH = /* glsl */ `
|
|
177
|
+
uniform sampler2D uMask;
|
|
178
|
+
uniform sampler2D uCoverage;
|
|
179
|
+
uniform vec2 uTexel;
|
|
180
|
+
uniform int uRadius;
|
|
181
|
+
// a masked pixel's colour and width, from the resolved mask and its coverage
|
|
182
|
+
vec4 masked(vec2 uv, float coverage) {
|
|
183
|
+
vec4 m = texture(uMask, uv) / coverage;
|
|
184
|
+
return vec4(m.rgb, m.a * ${OUTLINE_MAX}.0);
|
|
185
|
+
}
|
|
186
|
+
`
|
|
187
|
+
// across: each pixel notes which masked pixel along its row has the nearest edge, as an offset
|
|
188
|
+
const OUTLINE_SMOOTH_WIDEN_FRAGMENT = /* glsl */ `
|
|
189
|
+
precision highp float;
|
|
190
|
+
in vec2 vUv;
|
|
191
|
+
out vec4 fragColor;
|
|
192
|
+
${SMOOTH}
|
|
193
|
+
void main() {
|
|
194
|
+
float nearest = 1e9;
|
|
195
|
+
int at = 0;
|
|
196
|
+
bool found = false;
|
|
197
|
+
for (int i = -${OUTLINE_MAX + 1}; i <= ${OUTLINE_MAX + 1}; i++) {
|
|
198
|
+
if (abs(i) > uRadius + 1) continue;
|
|
199
|
+
vec2 uv = vUv + vec2(float(i) * uTexel.x, 0.0);
|
|
200
|
+
float c = texture(uCoverage, uv).r;
|
|
201
|
+
if (c <= 0.0) continue;
|
|
202
|
+
float d = abs(float(i)) - (c - 0.5);
|
|
203
|
+
if (d < masked(uv, c).a + 0.5 && d < nearest) {
|
|
204
|
+
nearest = d;
|
|
205
|
+
at = i;
|
|
206
|
+
found = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
fragColor = found ? vec4(float(at + 32) / 255.0, 0.0, 0.0, 1.0) : vec4(0.0);
|
|
210
|
+
}
|
|
211
|
+
`
|
|
212
|
+
// down, over the scene: the nearest of the rows' nearest, in true distance, faded at both edges
|
|
213
|
+
const OUTLINE_SMOOTH_COMPOSITE_FRAGMENT = /* glsl */ `
|
|
214
|
+
precision highp float;
|
|
215
|
+
in vec2 vUv;
|
|
216
|
+
uniform sampler2D uNearest;
|
|
217
|
+
out vec4 fragColor;
|
|
218
|
+
${COMMON}
|
|
219
|
+
${SMOOTH}
|
|
220
|
+
void main() {
|
|
221
|
+
float here = texture(uCoverage, vUv).r;
|
|
222
|
+
if (here >= 1.0) discard;
|
|
223
|
+
float nearest = 1e9;
|
|
224
|
+
vec4 ring = vec4(0.0);
|
|
225
|
+
for (int j = -${OUTLINE_MAX + 1}; j <= ${OUTLINE_MAX + 1}; j++) {
|
|
226
|
+
if (abs(j) > uRadius + 1) continue;
|
|
227
|
+
vec2 row = vUv + vec2(0.0, float(j) * uTexel.y);
|
|
228
|
+
vec4 n = texture(uNearest, row);
|
|
229
|
+
if (n.a <= 0.0) continue;
|
|
230
|
+
int i = int(n.r * 255.0 + 0.5) - 32;
|
|
231
|
+
vec2 uv = row + vec2(float(i) * uTexel.x, 0.0);
|
|
232
|
+
float c = texture(uCoverage, uv).r;
|
|
233
|
+
float d = length(vec2(float(i), float(j))) - (c - 0.5);
|
|
234
|
+
if (d < nearest) {
|
|
235
|
+
nearest = d;
|
|
236
|
+
ring = masked(uv, c);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
float alpha = clamp(ring.a - nearest + 0.5, 0.0, 1.0) * (1.0 - here);
|
|
240
|
+
if (alpha <= 0.0) discard;
|
|
241
|
+
fragColor = vec4(linearToSrgb(ring.rgb) * alpha, alpha);
|
|
242
|
+
}
|
|
243
|
+
`
|
|
157
244
|
const OUTLINE_MASK = /** @type {[string, string]} */ ([MESH_VERTEX, OUTLINE_MASK_FRAGMENT])
|
|
158
245
|
const OUTLINE_WIDEN = /** @type {[string, string]} */ ([FULLSCREEN_VERTEX, OUTLINE_WIDEN_FRAGMENT])
|
|
159
246
|
const OUTLINE_COMPOSITE = /** @type {[string, string]} */ ([FULLSCREEN_VERTEX, OUTLINE_COMPOSITE_FRAGMENT])
|
|
247
|
+
const OUTLINE_COVER = /** @type {[string, string]} */ ([MESH_VERTEX, OUTLINE_COVER_FRAGMENT])
|
|
248
|
+
const OUTLINE_SMOOTH_WIDEN = /** @type {[string, string]} */ ([FULLSCREEN_VERTEX, OUTLINE_SMOOTH_WIDEN_FRAGMENT])
|
|
249
|
+
const OUTLINE_SMOOTH_COMPOSITE = /** @type {[string, string]} */ ([FULLSCREEN_VERTEX, OUTLINE_SMOOTH_COMPOSITE_FRAGMENT])
|
|
160
250
|
|
|
161
251
|
// shadow maps only need depth - of what isn't cut away
|
|
162
252
|
const DEPTH = /** @type {[string, string]} */ ([MESH_VERTEX, CLIP_ONLY_FRAGMENT])
|
|
@@ -183,12 +273,19 @@ const ENV_LEVELS = 6
|
|
|
183
273
|
* @typedef {{ define: string, uniforms: Record<string, unknown>, sampler: string, texture: WebGLTexture }} ShadowMap
|
|
184
274
|
*/
|
|
185
275
|
|
|
276
|
+
/**
|
|
277
|
+
* @typedef {{ texture: WebGLTexture, framebuffer: WebGLFramebuffer }} Target
|
|
278
|
+
* @typedef {{ width: number, height: number, smooth: boolean, mask: Target, wide: Target, coverage: Target | null,
|
|
279
|
+
* msaa: { framebuffer: WebGLFramebuffer, renderbuffers: WebGLRenderbuffer[] } | null }} OutlineTargets
|
|
280
|
+
* @typedef {{ mask: WebGLTexture, wide: WebGLTexture, coverage: WebGLTexture | null, radius: number, width: number, height: number }} OutlineMask
|
|
281
|
+
*/
|
|
282
|
+
|
|
186
283
|
export class Passes {
|
|
187
284
|
/** @type {WeakMap<Texture, Environment>} */ #environments = new WeakMap()
|
|
188
285
|
/** @type {{ texture: WebGLTexture, framebuffer: WebGLFramebuffer } | null} */ #shadow = null
|
|
189
286
|
/** Spot lights' shadows, a tile each. @type {{ texture: WebGLTexture, framebuffer: WebGLFramebuffer } | null} */ #spotShadow = null
|
|
190
287
|
/** A point light's shadow: a depth cube, a framebuffer a face. @type {{ texture: WebGLTexture, framebuffers: WebGLFramebuffer[] } | null} */ #pointShadow = null
|
|
191
|
-
/** Outlines' mask and its widened copy. @type {
|
|
288
|
+
/** Outlines' mask and its widened copy. @type {OutlineTargets | null} */ #outline = null
|
|
192
289
|
/** Rendering to float textures, which environments need. */
|
|
193
290
|
floatTargets = false
|
|
194
291
|
|
|
@@ -413,6 +510,8 @@ export class Passes {
|
|
|
413
510
|
// read the map as the drawn surface reads it - projected, for a triplanar one
|
|
414
511
|
const triplanar = cutout && !!material.map && material.triplanar > 0
|
|
415
512
|
if (cutout) defines.push('ALPHA_TEST', ...(material.map ? ['MAP'] : []), ...(triplanar ? ['TRIPLANAR'] : []))
|
|
513
|
+
// a map on the second UV set reads it here too
|
|
514
|
+
if (cutout && material.map?.uvSet === 1 && item.geometry.uvs2) defines.push('UV2', 'uMapSet vUv2')
|
|
416
515
|
const program = this.programs.ready('depth', defines, DEPTH)
|
|
417
516
|
gl.useProgram(program.program)
|
|
418
517
|
program.set('uViewProjection', viewProjection)
|
|
@@ -422,8 +521,10 @@ export class Passes {
|
|
|
422
521
|
program.set('uOpacity', material.opacity)
|
|
423
522
|
program.set('uAlphaTest', material.alphaTest)
|
|
424
523
|
const map = material.map
|
|
425
|
-
|
|
426
|
-
|
|
524
|
+
if (map) {
|
|
525
|
+
program.set('uMapUv', uvMatrix(map))
|
|
526
|
+
program.set('uMap', this.resources.texture(map))
|
|
527
|
+
}
|
|
427
528
|
if (triplanar) program.set('uTriplanar', material.triplanar)
|
|
428
529
|
}
|
|
429
530
|
this.resources.drawTriangles(item.geometry, instances)
|
|
@@ -434,15 +535,19 @@ export class Passes {
|
|
|
434
535
|
* The first half of drawing outlines, before the scene: a mask of the outlined subtrees - each
|
|
435
536
|
* pixel they cover in their colour, with the line's width in alpha - then that mask widened
|
|
436
537
|
* sideways. outlineComposite() widens it up and down and draws the ring left round each mask.
|
|
538
|
+
* Smooth outlines draw the mask multisampled, with how much of each pixel it covers, and measure
|
|
539
|
+
* the ring from the shape's edge; otherwise each pixel is in or out.
|
|
437
540
|
* @param {FrameOutline[]} outlines @param {FrameDescription} frame
|
|
438
541
|
* @param {number} width @param {number} height drawing buffer pixels
|
|
439
542
|
* @param {number} pixelRatio
|
|
440
|
-
* @
|
|
543
|
+
* @param {boolean} smooth antialiased: soft edges and round corners
|
|
544
|
+
* @returns {OutlineMask}
|
|
441
545
|
*/
|
|
442
|
-
outlineMask(outlines, frame, width, height, pixelRatio) {
|
|
546
|
+
outlineMask(outlines, frame, width, height, pixelRatio, smooth) {
|
|
443
547
|
const gl = this.gl
|
|
444
|
-
const targets = this.#outlineTargets(width, height)
|
|
445
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, targets.mask.framebuffer)
|
|
548
|
+
const targets = this.#outlineTargets(width, height, smooth)
|
|
549
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, targets.msaa ? targets.msaa.framebuffer : targets.mask.framebuffer)
|
|
550
|
+
if (targets.msaa) gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1])
|
|
446
551
|
gl.viewport(0, 0, width, height)
|
|
447
552
|
gl.clearColor(0, 0, 0, 0)
|
|
448
553
|
gl.clear(gl.COLOR_BUFFER_BIT)
|
|
@@ -457,7 +562,7 @@ export class Passes {
|
|
|
457
562
|
for (const item of outline.items) {
|
|
458
563
|
const defines = item.instances ? ['INSTANCED'] : []
|
|
459
564
|
if (item.clip) defines.push('CLIP')
|
|
460
|
-
const program = this.programs.ready('outline-mask', defines, OUTLINE_MASK)
|
|
565
|
+
const program = smooth ? this.programs.ready('outline-cover', defines, OUTLINE_COVER) : this.programs.ready('outline-mask', defines, OUTLINE_MASK)
|
|
461
566
|
gl.useProgram(program.program)
|
|
462
567
|
program.set('uViewProjection', frame.camera.viewProjection)
|
|
463
568
|
program.set('uModel', item.worldMatrix)
|
|
@@ -466,35 +571,62 @@ export class Passes {
|
|
|
466
571
|
this.resources.drawTriangles(item.geometry, item.instances)
|
|
467
572
|
}
|
|
468
573
|
}
|
|
574
|
+
// the samples resolved: the mask into its texture, the coverage into its own
|
|
575
|
+
if (targets.msaa && targets.coverage) {
|
|
576
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, targets.msaa.framebuffer)
|
|
577
|
+
for (const [attachment, target] of /** @type {[number, Target][]} */ ([[gl.COLOR_ATTACHMENT0, targets.mask], [gl.COLOR_ATTACHMENT1, targets.coverage]])) {
|
|
578
|
+
gl.readBuffer(attachment)
|
|
579
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, target.framebuffer)
|
|
580
|
+
gl.blitFramebuffer(0, 0, width, height, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.NEAREST)
|
|
581
|
+
}
|
|
582
|
+
}
|
|
469
583
|
// widened sideways
|
|
470
584
|
gl.bindFramebuffer(gl.FRAMEBUFFER, targets.wide.framebuffer)
|
|
471
585
|
gl.clear(gl.COLOR_BUFFER_BIT)
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
586
|
+
if (targets.coverage) {
|
|
587
|
+
const widen = this.programs.ready('outline-smooth-widen', [], OUTLINE_SMOOTH_WIDEN)
|
|
588
|
+
gl.useProgram(widen.program)
|
|
589
|
+
widen.set('uMask', targets.mask.texture)
|
|
590
|
+
widen.set('uCoverage', targets.coverage.texture)
|
|
591
|
+
widen.set('uTexel', [1 / width, 1 / height])
|
|
592
|
+
widen.set('uRadius', radius)
|
|
593
|
+
} else {
|
|
594
|
+
const widen = this.programs.ready('outline-widen', [], OUTLINE_WIDEN)
|
|
595
|
+
gl.useProgram(widen.program)
|
|
596
|
+
widen.set('uSource', targets.mask.texture)
|
|
597
|
+
widen.set('uStep', [1 / width, 0])
|
|
598
|
+
widen.set('uRadius', radius)
|
|
599
|
+
}
|
|
477
600
|
this.programs.drawFullscreen()
|
|
478
601
|
gl.depthMask(true)
|
|
479
602
|
gl.enable(gl.DEPTH_TEST)
|
|
480
603
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
|
|
481
|
-
return { mask: targets.mask.texture, wide: targets.wide.texture, radius }
|
|
604
|
+
return { mask: targets.mask.texture, wide: targets.wide.texture, coverage: targets.coverage?.texture ?? null, radius, width, height }
|
|
482
605
|
}
|
|
483
606
|
|
|
484
607
|
/**
|
|
485
608
|
* Draw the outlines over the finished scene, into whatever is bound: the sideways-widened mask
|
|
486
609
|
* widened up and down too, less the mask itself, so a ring is left round each subtree.
|
|
487
|
-
* @param {
|
|
488
|
-
* @param {number} height drawing buffer pixels
|
|
610
|
+
* @param {OutlineMask} outline
|
|
489
611
|
*/
|
|
490
|
-
outlineComposite(outline
|
|
612
|
+
outlineComposite(outline) {
|
|
491
613
|
const gl = this.gl
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
614
|
+
if (outline.coverage) {
|
|
615
|
+
const program = this.programs.ready('outline-smooth-composite', [], OUTLINE_SMOOTH_COMPOSITE)
|
|
616
|
+
gl.useProgram(program.program)
|
|
617
|
+
program.set('uNearest', outline.wide)
|
|
618
|
+
program.set('uMask', outline.mask)
|
|
619
|
+
program.set('uCoverage', outline.coverage)
|
|
620
|
+
program.set('uTexel', [1 / outline.width, 1 / outline.height])
|
|
621
|
+
program.set('uRadius', outline.radius)
|
|
622
|
+
} else {
|
|
623
|
+
const program = this.programs.ready('outline-composite', [], OUTLINE_COMPOSITE)
|
|
624
|
+
gl.useProgram(program.program)
|
|
625
|
+
program.set('uSource', outline.wide)
|
|
626
|
+
program.set('uMask', outline.mask)
|
|
627
|
+
program.set('uStep', [0, 1 / outline.height])
|
|
628
|
+
program.set('uRadius', outline.radius)
|
|
629
|
+
}
|
|
498
630
|
gl.disable(gl.DEPTH_TEST)
|
|
499
631
|
gl.disable(gl.CULL_FACE)
|
|
500
632
|
gl.depthMask(false)
|
|
@@ -505,22 +637,32 @@ export class Passes {
|
|
|
505
637
|
gl.enable(gl.DEPTH_TEST)
|
|
506
638
|
}
|
|
507
639
|
|
|
508
|
-
/**
|
|
509
|
-
|
|
640
|
+
/**
|
|
641
|
+
* The mask and its widened copy - and, for smooth outlines, the multisampled buffers drawn into
|
|
642
|
+
* and the resolved coverage - made again when the size or the kind changes.
|
|
643
|
+
* @param {number} width @param {number} height @param {boolean} smooth
|
|
644
|
+
* @returns {OutlineTargets}
|
|
645
|
+
*/
|
|
646
|
+
#outlineTargets(width, height, smooth) {
|
|
510
647
|
const t = this.#outline
|
|
511
|
-
if (t && t.width === width && t.height === height) return t
|
|
648
|
+
if (t && t.width === width && t.height === height && t.smooth === smooth) return t
|
|
512
649
|
const gl = this.gl
|
|
513
650
|
if (t) {
|
|
514
|
-
for (const
|
|
515
|
-
|
|
516
|
-
gl.
|
|
651
|
+
for (const target of [t.mask, t.wide, t.coverage]) {
|
|
652
|
+
if (!target) continue
|
|
653
|
+
gl.deleteTexture(target.texture)
|
|
654
|
+
gl.deleteFramebuffer(target.framebuffer)
|
|
655
|
+
}
|
|
656
|
+
if (t.msaa) {
|
|
657
|
+
gl.deleteFramebuffer(t.msaa.framebuffer)
|
|
658
|
+
for (const buffer of t.msaa.renderbuffers) gl.deleteRenderbuffer(buffer)
|
|
517
659
|
}
|
|
518
660
|
}
|
|
519
|
-
const make = () => {
|
|
661
|
+
const make = (/** @type {number} */ format = gl.RGBA8) => {
|
|
520
662
|
const texture = /** @type {WebGLTexture} */ (gl.createTexture())
|
|
521
663
|
gl.activeTexture(gl.TEXTURE15)
|
|
522
664
|
gl.bindTexture(gl.TEXTURE_2D, texture)
|
|
523
|
-
gl.texStorage2D(gl.TEXTURE_2D, 1,
|
|
665
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, format, width, height)
|
|
524
666
|
// read texel for texel: no blending between neighbours
|
|
525
667
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
|
526
668
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
|
@@ -531,7 +673,23 @@ export class Passes {
|
|
|
531
673
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)
|
|
532
674
|
return { texture, framebuffer }
|
|
533
675
|
}
|
|
534
|
-
|
|
676
|
+
/** @type {OutlineTargets['msaa']} */
|
|
677
|
+
let msaa = null
|
|
678
|
+
if (smooth) {
|
|
679
|
+
// two multisampled targets drawn at once: the mask, and its coverage
|
|
680
|
+
const samples = Math.min(4, gl.getParameter(gl.MAX_SAMPLES))
|
|
681
|
+
const framebuffer = /** @type {WebGLFramebuffer} */ (gl.createFramebuffer())
|
|
682
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)
|
|
683
|
+
const renderbuffers = [gl.RGBA8, gl.R8].map((format, i) => {
|
|
684
|
+
const buffer = /** @type {WebGLRenderbuffer} */ (gl.createRenderbuffer())
|
|
685
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, buffer)
|
|
686
|
+
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, format, width, height)
|
|
687
|
+
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.RENDERBUFFER, buffer)
|
|
688
|
+
return buffer
|
|
689
|
+
})
|
|
690
|
+
msaa = { framebuffer, renderbuffers }
|
|
691
|
+
}
|
|
692
|
+
this.#outline = { width, height, smooth, mask: make(), wide: make(), coverage: smooth ? make(gl.R8) : null, msaa }
|
|
535
693
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
|
|
536
694
|
return this.#outline
|
|
537
695
|
}
|
package/src/webgl/picking.js
CHANGED
|
@@ -232,7 +232,6 @@ export class Picker {
|
|
|
232
232
|
program.set('uModel', worldMatrix)
|
|
233
233
|
program.set('uPickId', id)
|
|
234
234
|
program.set('uHighlightIndex', -2)
|
|
235
|
-
program.set('uUvTransform', [1, 1, 0, 0])
|
|
236
235
|
if (item.clip) program.set('uClipPlane', item.clip.plane)
|
|
237
236
|
|
|
238
237
|
if (kind === 'lines') {
|
package/src/webgl/resources.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// CPU objects through their `version` counters. Everything is keyed by object in WeakMaps, so a
|
|
3
3
|
// dropped geometry's entry goes with it; dispose() frees the GL objects straight away.
|
|
4
4
|
import { ATTRIBUTES } from './shaders.js'
|
|
5
|
+
import { isVideo } from '../material/Texture.js'
|
|
5
6
|
|
|
6
7
|
/** @import { Geometry } from '../geometry/Geometry.js' */
|
|
7
8
|
/** @import { Texture, DataImage } from '../material/Texture.js' */
|
|
@@ -31,9 +32,79 @@ const UPLOAD_UNIT = 15
|
|
|
31
32
|
* @param {{ instanceMatrices: Float32Array, instanceColors: Float32Array | null } | null} instances
|
|
32
33
|
* @param {boolean} withNormals
|
|
33
34
|
*/
|
|
34
|
-
function layoutOf(g, instances, withNormals) {
|
|
35
|
-
return [g.positions.length, withNormals, g.uvs?.length, g.colors?.length, g.indices?.length,
|
|
36
|
-
instances?.instanceMatrices.length, instances?.instanceColors?.length].join()
|
|
35
|
+
function layoutOf(g, instances, withNormals, withTangents) {
|
|
36
|
+
return [g.positions.length, withNormals, g.uvs?.length, g.uvs2?.length, g.colors?.length, g.indices?.length,
|
|
37
|
+
instances?.instanceMatrices.length, instances?.instanceColors?.length, withTangents].join()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Per-vertex tangents along the UVs' u, with the way v runs in w (Lengyel's method): smooth where
|
|
42
|
+
* the normals are. Without UVs, around the mesh's up axis.
|
|
43
|
+
* @param {Geometry} g
|
|
44
|
+
* @returns {Float32Array}
|
|
45
|
+
*/
|
|
46
|
+
export function tangentsOf(g) {
|
|
47
|
+
const p = g.positions, n = g.normals, uv = g.uvs, count = p.length / 3
|
|
48
|
+
const out = new Float32Array(count * 4)
|
|
49
|
+
const t = new Float32Array(count * 3), b = new Float32Array(count * 3)
|
|
50
|
+
if (uv) {
|
|
51
|
+
const idx = g.indices, triangles = (idx ? idx.length : count) / 3
|
|
52
|
+
for (let k = 0; k < triangles; k++) {
|
|
53
|
+
const i0 = idx ? idx[k * 3] : k * 3, i1 = idx ? idx[k * 3 + 1] : k * 3 + 1, i2 = idx ? idx[k * 3 + 2] : k * 3 + 2
|
|
54
|
+
const e1x = p[i1 * 3] - p[i0 * 3], e1y = p[i1 * 3 + 1] - p[i0 * 3 + 1], e1z = p[i1 * 3 + 2] - p[i0 * 3 + 2]
|
|
55
|
+
const e2x = p[i2 * 3] - p[i0 * 3], e2y = p[i2 * 3 + 1] - p[i0 * 3 + 1], e2z = p[i2 * 3 + 2] - p[i0 * 3 + 2]
|
|
56
|
+
const du1 = uv[i1 * 2] - uv[i0 * 2], dv1 = uv[i1 * 2 + 1] - uv[i0 * 2 + 1]
|
|
57
|
+
const du2 = uv[i2 * 2] - uv[i0 * 2], dv2 = uv[i2 * 2 + 1] - uv[i0 * 2 + 1]
|
|
58
|
+
const det = du1 * dv2 - du2 * dv1
|
|
59
|
+
if (Math.abs(det) < 1e-12) continue
|
|
60
|
+
const r = 1 / det
|
|
61
|
+
const tx = (e1x * dv2 - e2x * dv1) * r, ty = (e1y * dv2 - e2y * dv1) * r, tz = (e1z * dv2 - e2z * dv1) * r
|
|
62
|
+
const bx = (e2x * du1 - e1x * du2) * r, by = (e2y * du1 - e1y * du2) * r, bz = (e2z * du1 - e1z * du2) * r
|
|
63
|
+
for (const i of [i0, i1, i2]) {
|
|
64
|
+
t[i * 3] += tx; t[i * 3 + 1] += ty; t[i * 3 + 2] += tz
|
|
65
|
+
b[i * 3] += bx; b[i * 3 + 1] += by; b[i * 3 + 2] += bz
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (let i = 0; i < count; i++) {
|
|
70
|
+
const nx = n[i * 3], ny = n[i * 3 + 1], nz = n[i * 3 + 2]
|
|
71
|
+
let tx = t[i * 3], ty = t[i * 3 + 1], tz = t[i * 3 + 2]
|
|
72
|
+
// onto the surface, square to the normal
|
|
73
|
+
const d = nx * tx + ny * ty + nz * tz
|
|
74
|
+
tx -= nx * d; ty -= ny * d; tz -= nz * d
|
|
75
|
+
let len = Math.hypot(tx, ty, tz)
|
|
76
|
+
if (len < 1e-9) {
|
|
77
|
+
// no UVs here: around the up axis, or the x axis for a surface facing up or down
|
|
78
|
+
if (Math.abs(ny) < 0.999) { tx = nz; ty = 0; tz = -nx } else { tx = 1; ty = 0; tz = 0 }
|
|
79
|
+
const e = nx * tx + ny * ty + nz * tz
|
|
80
|
+
tx -= nx * e; ty -= ny * e; tz -= nz * e
|
|
81
|
+
len = Math.hypot(tx, ty, tz) || 1
|
|
82
|
+
}
|
|
83
|
+
out[i * 4] = tx / len; out[i * 4 + 1] = ty / len; out[i * 4 + 2] = tz / len
|
|
84
|
+
// which way v runs against n x t: mirrored UVs turn the other way
|
|
85
|
+
const cx = ny * tz - nz * ty, cy = nz * tx - nx * tz, cz = nx * ty - ny * tx
|
|
86
|
+
out[i * 4 + 3] = cx * b[i * 3] + cy * b[i * 3 + 1] + cz * b[i * 3 + 2] < 0 ? -1 : 1
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const UV_MATRIX = new Float32Array(9)
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A map's UV transform as a 3x3 (column-major): repeat, then rotation about its centre, then offset -
|
|
95
|
+
* glTF's KHR_texture_transform order, turning the way three.js and glTF viewers do. Written into one
|
|
96
|
+
* shared array, so set it as a uniform straight away.
|
|
97
|
+
* @param {{ repeat: number[], offset: number[], rotation: number, center: number[] }} t
|
|
98
|
+
* @returns {Float32Array}
|
|
99
|
+
*/
|
|
100
|
+
export function uvMatrix(t) {
|
|
101
|
+
const c = Math.cos(t.rotation), s = Math.sin(t.rotation)
|
|
102
|
+
const [sx, sy] = t.repeat, [cx, cy] = t.center
|
|
103
|
+
const m = UV_MATRIX
|
|
104
|
+
m[0] = c * sx; m[1] = -s * sx; m[2] = 0
|
|
105
|
+
m[3] = s * sy; m[4] = c * sy; m[5] = 0
|
|
106
|
+
m[6] = cx - c * cx - s * cy + t.offset[0]; m[7] = cy + s * cx - c * cy + t.offset[1]; m[8] = 1
|
|
107
|
+
return m
|
|
37
108
|
}
|
|
38
109
|
|
|
39
110
|
// a quad as a triangle strip of [along, across] corners, shared by every Lines draw
|
|
@@ -78,17 +149,20 @@ export class Resources {
|
|
|
78
149
|
* @param {Geometry} geometry
|
|
79
150
|
* @param {{ instanceMatrices: Float32Array, instanceColors: Float32Array | null, version: number } | null} instances
|
|
80
151
|
* @param {boolean} withNormals
|
|
152
|
+
* @param {boolean} [withTangents] add tangents, for brushed metal - kept once added
|
|
81
153
|
* @returns {MeshBuffers}
|
|
82
154
|
*/
|
|
83
|
-
mesh(geometry, instances, withNormals) {
|
|
155
|
+
mesh(geometry, instances, withNormals, withTangents = false) {
|
|
84
156
|
const key = instances ?? geometry
|
|
85
157
|
this.#mark(key)
|
|
86
158
|
const cache = withNormals ? this.#triangles : this.#points
|
|
87
159
|
let entry = cache.get(key)
|
|
88
160
|
// instanced buffers are kept by the node, so check it still has the same geometry
|
|
89
161
|
const same = entry?.geometry === geometry
|
|
90
|
-
|
|
91
|
-
|
|
162
|
+
withTangents = withNormals && (withTangents || (same && !!entry?.attributes?.tangent))
|
|
163
|
+
if (entry && same && entry.version === geometry.version && (!instances || entry.instanceVersion === instances.version) &&
|
|
164
|
+
withTangents === !!entry.attributes?.tangent) return entry
|
|
165
|
+
const layout = layoutOf(geometry, instances, withNormals, withTangents)
|
|
92
166
|
if (entry && same && entry.layout === layout) {
|
|
93
167
|
// the data changed but kept its shape (moving vertices, particles, instances): refill in place
|
|
94
168
|
if (entry.version !== geometry.version) {
|
|
@@ -96,7 +170,9 @@ export class Resources {
|
|
|
96
170
|
this.#refill(a.position, geometry.positions)
|
|
97
171
|
if (withNormals) this.#refill(a.normal, geometry.normals)
|
|
98
172
|
if (geometry.uvs) this.#refill(a.uv, geometry.uvs)
|
|
173
|
+
if (geometry.uvs2) this.#refill(a.uv2, geometry.uvs2)
|
|
99
174
|
if (geometry.colors) this.#refill(a.color, geometry.colors)
|
|
175
|
+
if (withTangents) this.#refill(a.tangent, tangentsOf(geometry))
|
|
100
176
|
if (geometry.indices) {
|
|
101
177
|
// the element binding belongs to the vertex array, so bind that first
|
|
102
178
|
this.gl.bindVertexArray(entry.vao)
|
|
@@ -142,8 +218,10 @@ export class Resources {
|
|
|
142
218
|
if (withNormals) attributes.normal = attribute(ATTRIBUTES.normal, geometry.normals, 3)
|
|
143
219
|
if (geometry.uvs) attributes.uv = attribute(ATTRIBUTES.uv, geometry.uvs, 2)
|
|
144
220
|
else gl.vertexAttrib2f(ATTRIBUTES.uv, 0, 0)
|
|
221
|
+
if (geometry.uvs2) attributes.uv2 = attribute(ATTRIBUTES.uv2, geometry.uvs2, 2)
|
|
145
222
|
if (geometry.colors) attributes.color = attribute(ATTRIBUTES.color, geometry.colors, 3)
|
|
146
223
|
else gl.vertexAttrib3f(ATTRIBUTES.color, 1, 1, 1)
|
|
224
|
+
if (withTangents) attributes.tangent = attribute(ATTRIBUTES.tangent, tangentsOf(geometry), 4)
|
|
147
225
|
|
|
148
226
|
let matrixBuffer = null, colorBuffer = null
|
|
149
227
|
if (instances) {
|
|
@@ -181,11 +259,12 @@ export class Resources {
|
|
|
181
259
|
* Draw a mesh's triangles - instanced or not, indexed or not - with the program in use, as far as
|
|
182
260
|
* its drawn count goes. Every pass that draws meshes draws them this way.
|
|
183
261
|
* @param {Geometry} geometry @param {{ instanceMatrices: Float32Array, instanceColors: Float32Array | null, version: number, count: number } | null} instances
|
|
262
|
+
* @param {boolean} [withTangents] for brushed metal
|
|
184
263
|
* @returns {number} triangles drawn
|
|
185
264
|
*/
|
|
186
|
-
drawTriangles(geometry, instances) {
|
|
265
|
+
drawTriangles(geometry, instances, withTangents = false) {
|
|
187
266
|
const gl = this.gl
|
|
188
|
-
const buffers = this.mesh(geometry, instances, true)
|
|
267
|
+
const buffers = this.mesh(geometry, instances, true, withTangents)
|
|
189
268
|
gl.bindVertexArray(buffers.vao)
|
|
190
269
|
const elements = geometry.drawnCount - geometry.drawnCount % 3
|
|
191
270
|
const count = instances ? instances.count : 1
|
|
@@ -272,6 +351,8 @@ export class Resources {
|
|
|
272
351
|
const gl = this.gl
|
|
273
352
|
let entry = this.#textures.get(texture)
|
|
274
353
|
if (entry && entry.version === texture.version) return entry.texture
|
|
354
|
+
// a video with no frame yet: what it showed before, or white - and try again next frame
|
|
355
|
+
if (isVideo(texture.image) && /** @type {HTMLVideoElement} */ (texture.image).readyState < 2) return entry?.texture ?? this.#whiteTexture()
|
|
275
356
|
if (!entry) {
|
|
276
357
|
entry = { texture: /** @type {WebGLTexture} */ (gl.createTexture()), version: -1 }
|
|
277
358
|
this.#textures.set(texture, entry)
|