phoria2 1.2.0 → 1.3.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 +2 -3
- package/dist/charts.js +476 -459
- package/dist/charts.js.map +1 -1
- package/dist/core.js +1192 -963
- package/dist/core.js.map +1 -1
- package/dist/edit.js +75 -75
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +1 -1
- package/dist/gltf.js +84 -84
- package/dist/gltf.js.map +1 -1
- package/dist/keyboard.js +188 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/motion.js +35 -35
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +191 -118
- package/dist/particles.js.map +1 -1
- package/dist/passes.js +16 -16
- package/dist/passes.js.map +1 -1
- package/dist/phoria2.js +1 -1
- package/dist/picking.js +30 -14
- package/dist/picking.js.map +1 -1
- package/dist/snapshot.js +1 -1
- package/dist/tooltip.js +1 -1
- package/dist/types/View.d.ts +11 -0
- package/dist/types/area.d.ts +18 -0
- package/dist/types/geometry/Geometry.d.ts +16 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/material/Material.d.ts +15 -0
- package/dist/types/material/Texture.d.ts +18 -2
- package/dist/types/particles/Emitter.d.ts +39 -2
- package/dist/types/scene/drawables.d.ts +23 -2
- package/dist/types/webgl/resources.d.ts +12 -2
- package/dist/types/webgl/shaders.d.ts +7 -2
- package/llms-full.txt +48 -1
- package/package.json +1 -1
- package/schema/scene.schema.json +48 -0
- package/src/View.js +16 -0
- package/src/area.js +131 -0
- package/src/charts/marks.js +29 -2
- package/src/geometry/Geometry.js +16 -1
- package/src/index.js +1 -0
- package/src/material/Material.js +9 -2
- package/src/material/Texture.js +55 -2
- package/src/particles/Emitter.js +121 -10
- package/src/scene/drawables.js +68 -4
- package/src/watch.js +8 -1
- package/src/webgl/WebGLRenderer.js +36 -5
- package/src/webgl/picking.js +22 -1
- package/src/webgl/resources.js +168 -14
- package/src/webgl/shaders.js +110 -11
package/src/index.js
CHANGED
|
@@ -50,6 +50,7 @@ export { PhoriaError } from './validate.js'
|
|
|
50
50
|
/** @typedef {import('./presets.js').CameraPreset} CameraPreset */
|
|
51
51
|
/** @typedef {import('./tooltip.js').TooltipContent} TooltipContent */
|
|
52
52
|
/** @typedef {import('./Raycaster.js').Hit} Hit */
|
|
53
|
+
/** @typedef {import('./area.js').PickArea} PickArea */
|
|
53
54
|
/** @typedef {import('./scene/drawables.js').LabelPlacement} LabelPlacement */
|
|
54
55
|
/** @typedef {import('./scene/drawables.js').LabelPlace} LabelPlace */
|
|
55
56
|
/** @typedef {import('./scene/drawables.js').InstanceLabels} InstanceLabels */
|
package/src/material/Material.js
CHANGED
|
@@ -72,6 +72,9 @@ import { Texture } from './Texture.js'
|
|
|
72
72
|
* @property {number} [size] Points: diameter in world units, or pixels when sizeAttenuation is off (default 1)
|
|
73
73
|
* @property {boolean} [sizeAttenuation] Points: sized in the world, so smaller with distance (default true)
|
|
74
74
|
* @property {number} [width] Lines: width in pixels (default 1)
|
|
75
|
+
* @property {number} [dash] Lines: dashed, with dashes this long in the model's units; 0 is solid (default 0)
|
|
76
|
+
* @property {number} [gap] Lines: the gaps between dashes; 0 is as long as the dashes (default 0)
|
|
77
|
+
* @property {number} [dashOffset] Lines: moves the dashes along the lines - animate it for flow (default 0)
|
|
75
78
|
*/
|
|
76
79
|
|
|
77
80
|
// the matcap last: the first map's repeat and offset are every map's, and a matcap has none
|
|
@@ -83,7 +86,8 @@ const NUMBERS = { opacity: [0, 1], roughness: [0, 1], metalness: [0, 1], emissiv
|
|
|
83
86
|
alphaTest: [0, 1], normalScale: [-Infinity, Infinity], aoMapIntensity: [0, 1], triplanar: [0, Infinity], bumpScale: [-Infinity, Infinity],
|
|
84
87
|
transmission: [0, 1], clearcoat: [0, 1], clearcoatRoughness: [0, 1], sheen: [0, 1], sheenRoughness: [0, 1], ior: [1, Infinity],
|
|
85
88
|
specularIntensity: [0, 1], anisotropy: [0, 1], anisotropyRotation: [-Infinity, Infinity], translucency: [0, 1], toon: [0, 16],
|
|
86
|
-
lightMapIntensity: [0, Infinity], environmentIntensity: [0, Infinity]
|
|
89
|
+
lightMapIntensity: [0, Infinity], environmentIntensity: [0, Infinity], dash: [0, Infinity], gap: [0, Infinity],
|
|
90
|
+
dashOffset: [-Infinity, Infinity] }
|
|
87
91
|
// colours, and their defaults
|
|
88
92
|
const COLORS = { color: 0xffffff, emissive: 0x000000, sheenColor: 0xffffff, specularColor: 0xffffff, translucencyColor: 0xffffff }
|
|
89
93
|
const DEFAULTS = {
|
|
@@ -91,7 +95,7 @@ const DEFAULTS = {
|
|
|
91
95
|
transmission: 0, clearcoat: 0, clearcoatRoughness: 0, sheen: 0, sheenRoughness: 0.5, ior: 1.5, specularIntensity: 1, anisotropy: 0,
|
|
92
96
|
anisotropyRotation: 0, translucency: 0, toon: 0, lightMapIntensity: 1, environmentIntensity: 1, unlit: false, flatShading: false,
|
|
93
97
|
doubleSided: false, vertexColors: false, blending: 'normal', depthWrite: true, depthTest: true, fog: true,
|
|
94
|
-
size: 1, sizeAttenuation: true, width: 1,
|
|
98
|
+
size: 1, sizeAttenuation: true, width: 1, dash: 0, gap: 0, dashOffset: 0,
|
|
95
99
|
}
|
|
96
100
|
const OPTIONS = ['name', ...Object.keys(COLORS), ...MAPS, ...FLAGS, ...Object.keys(NUMBERS), 'blending']
|
|
97
101
|
|
|
@@ -161,6 +165,9 @@ export class Material {
|
|
|
161
165
|
/** @type {number} */ size
|
|
162
166
|
/** @type {boolean} */ sizeAttenuation
|
|
163
167
|
/** @type {number} */ width
|
|
168
|
+
/** Lines: dash length in the model's units; 0 is solid. @type {number} */ dash
|
|
169
|
+
/** Lines: gap length; 0 is the dash's. @type {number} */ gap
|
|
170
|
+
/** Lines: how far the dashes have moved along. @type {number} */ dashOffset
|
|
164
171
|
|
|
165
172
|
/** @type {Record<string, Color>} */
|
|
166
173
|
#colors = Object.fromEntries(Object.entries(COLORS).map(([key, hex]) => [key, new Color(hex)]))
|
package/src/material/Texture.js
CHANGED
|
@@ -43,9 +43,18 @@ const HINTS = {
|
|
|
43
43
|
scale: "use 'repeat' - [u, v] times across",
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Part of an image, in its pixels from the top left, as needsUpdate() takes it.
|
|
48
|
+
* @typedef {{ x: number, y: number, width: number, height: number }} TextureRegion
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
// how many updates a texture remembers the region of, for a renderer catching up on several - a
|
|
52
|
+
// frame of paint can make a hundred or so
|
|
53
|
+
const CHANGES_KEPT = 256
|
|
54
|
+
|
|
46
55
|
/**
|
|
47
56
|
* An image for a material map. A video's frames upload by themselves as it plays; after drawing on
|
|
48
|
-
* a canvas, call `needsUpdate()
|
|
57
|
+
* a canvas, call `needsUpdate()` - with the rectangle drawn in, to send only that.
|
|
49
58
|
*/
|
|
50
59
|
export class Texture {
|
|
51
60
|
/** The options it takes, and hints for names people reach for instead. */
|
|
@@ -77,6 +86,8 @@ export class Texture {
|
|
|
77
86
|
flipY = false
|
|
78
87
|
/** @internal Bumped by needsUpdate() so renderers re-upload. */
|
|
79
88
|
version = 0
|
|
89
|
+
/** What each recent update changed - its region, or null for all of it. @type {{ version: number, region: TextureRegion | null }[]} */
|
|
90
|
+
#changes = []
|
|
80
91
|
/** Resolves when the image is available (immediately when one was given). @type {Promise<Texture>} */
|
|
81
92
|
ready
|
|
82
93
|
|
|
@@ -180,11 +191,53 @@ export class Texture {
|
|
|
180
191
|
/** @internal Whether the app chose the colour space, rather than leaving the default. @returns {boolean} */
|
|
181
192
|
get colorSpaceSet() { return this.#colorSpaceSet }
|
|
182
193
|
|
|
183
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Upload the image again, after drawing on it (a canvas) or changing its pixels. Give the
|
|
196
|
+
* rectangle that changed - in the image's pixels, from its top left - and only that is sent: for a
|
|
197
|
+
* big canvas drawn on a little at a time, far less to send each frame.
|
|
198
|
+
*
|
|
199
|
+
* ctx.fillRect(x, y, 20, 20)
|
|
200
|
+
* texture.needsUpdate({ x, y, width: 20, height: 20 })
|
|
201
|
+
* @param {TextureRegion} [region]
|
|
202
|
+
* @returns {this}
|
|
203
|
+
*/
|
|
204
|
+
needsUpdate(region) {
|
|
205
|
+
/** @type {TextureRegion | null} */
|
|
206
|
+
let changed = null
|
|
207
|
+
if (region !== undefined) {
|
|
208
|
+
const { x, y, width, height } = region ?? /** @type {any} */ ({})
|
|
209
|
+
if (![x, y, width, height].every((v) => typeof v === 'number' && Number.isFinite(v)) || width < 0 || height < 0) {
|
|
210
|
+
fail('Texture', `needsUpdate() takes the region changed, { x, y, width, height } in the image's pixels, got ${describe(region)}`)
|
|
211
|
+
}
|
|
212
|
+
// whole pixels, covering every one the region touches
|
|
213
|
+
const x0 = Math.floor(x), y0 = Math.floor(y)
|
|
214
|
+
changed = { x: x0, y: y0, width: Math.ceil(x + width) - x0, height: Math.ceil(y + height) - y0 }
|
|
215
|
+
}
|
|
184
216
|
this.version++
|
|
217
|
+
this.#changes.push({ version: this.version, region: changed })
|
|
218
|
+
if (this.#changes.length > CHANGES_KEPT) this.#changes.shift()
|
|
185
219
|
return this
|
|
186
220
|
}
|
|
187
221
|
|
|
222
|
+
/**
|
|
223
|
+
* @internal What changed since a version a renderer uploaded: each update's region since, or null
|
|
224
|
+
* when that's all of it - an update without a region, or one too long ago to know.
|
|
225
|
+
* @param {number} version
|
|
226
|
+
* @returns {TextureRegion[] | null}
|
|
227
|
+
*/
|
|
228
|
+
changedSince(version) {
|
|
229
|
+
const first = this.#changes[0]
|
|
230
|
+
if (!first || first.version > version + 1) return null
|
|
231
|
+
/** @type {TextureRegion[]} */
|
|
232
|
+
const regions = []
|
|
233
|
+
for (const { version: v, region } of this.#changes) {
|
|
234
|
+
if (v <= version) continue
|
|
235
|
+
if (!region) return null
|
|
236
|
+
regions.push(region)
|
|
237
|
+
}
|
|
238
|
+
return regions.length ? regions : null
|
|
239
|
+
}
|
|
240
|
+
|
|
188
241
|
/** Release the GPU copy. @returns {void} */
|
|
189
242
|
dispose() {
|
|
190
243
|
for (const fn of this.#onDispose) fn()
|
package/src/particles/Emitter.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// A particle system as one Points node: particles are born at the emitter's origin, move under
|
|
2
2
|
// velocity, gravity and drag, fade through a list of colours and die. The simulation runs in the
|
|
3
3
|
// emitter's own update handler, so it follows the scene clock (frame-rate independent) and its
|
|
4
|
-
// arrays go to the GPU as one buffer refill per frame.
|
|
4
|
+
// arrays go to the GPU as one buffer refill per frame. Particles that grow, fade or turn give the
|
|
5
|
+
// geometry per-point sizes, opacities or angles, and are drawn as squares.
|
|
5
6
|
import * as mat4 from '../math/mat4.js'
|
|
6
7
|
import { Geometry } from '../geometry/Geometry.js'
|
|
7
8
|
import { Material } from '../material/Material.js'
|
|
@@ -48,17 +49,32 @@ import { checkNumber, describe, fail } from '../validate.js'
|
|
|
48
49
|
* frustumCulled?: boolean,
|
|
49
50
|
* renderOrder?: number,
|
|
50
51
|
* onParticle?: ((p: Particle) => boolean | void) | null,
|
|
51
|
-
*
|
|
52
|
+
* sort?: boolean,
|
|
53
|
+
* } & LookOptions} EmitterOptions
|
|
52
54
|
* max: most live particles at once (default 1000). rate: particles born per second (default 100).
|
|
53
55
|
* gravity: acceleration, e.g. [0, -9.8, 0] (default none). drag: share of velocity lost per second
|
|
54
56
|
* (default 0). follow: a node whose position becomes the origin each frame - a trail that stays
|
|
55
57
|
* behind it. onParticle: called for every live particle each frame after it moves; return false to
|
|
56
|
-
* remove it (a particle hitting something).
|
|
58
|
+
* remove it (a particle hitting something). sort: draw furthest first, for soft particles with
|
|
59
|
+
* blending 'normal' (smoke) that would otherwise layer wrongly.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How particles look over their lives, beyond colour.
|
|
64
|
+
* @typedef {object} LookOptions
|
|
65
|
+
* @property {number | number[]} [size] a multiple of the material's size, or several each particle goes
|
|
66
|
+
* through over its life - [0.5, 3] for smoke that billows out (default 1)
|
|
67
|
+
* @property {number} [sizeSpread] each particle's size varies by up to this share either way, 0-1 (default 0)
|
|
68
|
+
* @property {number | number[]} [opacity] 0-1, or several each particle goes through - [0, 1, 0] fades in
|
|
69
|
+
* and out (default 1)
|
|
70
|
+
* @property {number | [number, number]} [spin] radians per second: a number turns each particle up to that
|
|
71
|
+
* fast either way, [min, max] a range. Spinning particles start at a random angle (default 0)
|
|
57
72
|
*/
|
|
58
73
|
|
|
59
74
|
const SPAWN_OPTIONS = ['origin', 'originSpread', 'velocity', 'velocitySpread', 'lifetime', 'color']
|
|
60
|
-
const
|
|
61
|
-
|
|
75
|
+
const LOOK_OPTIONS = ['size', 'sizeSpread', 'opacity', 'spin']
|
|
76
|
+
const BEHAVIOUR_OPTIONS = ['rate', 'gravity', 'drag', 'follow', 'onParticle', ...LOOK_OPTIONS]
|
|
77
|
+
const OPTIONS = [...Points.OPTIONS, ...SPAWN_OPTIONS, 'max', ...BEHAVIOUR_OPTIONS]
|
|
62
78
|
const HINTS = {
|
|
63
79
|
count: "use 'max' for the most particles alive at once, 'rate' for how many are born per second",
|
|
64
80
|
lifetimeRnd: "give 'lifetime' a [min, max] range",
|
|
@@ -111,6 +127,13 @@ export class Emitter extends Points {
|
|
|
111
127
|
/** @type {Map<string, number>} */ #paletteIds = new Map()
|
|
112
128
|
/** @type {Uint8Array} */ #palette
|
|
113
129
|
#spawnPalette = 0
|
|
130
|
+
// the looks: stops over each particle's life, and each one's own size factor and turn rate
|
|
131
|
+
/** @type {number[]} */ #size = [1]
|
|
132
|
+
#sizeSpread = 0
|
|
133
|
+
/** @type {number[]} */ #opacity = [1]
|
|
134
|
+
/** @type {[number, number]} */ #spin = [0, 0]
|
|
135
|
+
/** @type {Float32Array | null} */ #sizeFactors = null
|
|
136
|
+
/** @type {Float32Array | null} */ #spins = null
|
|
114
137
|
/** Last frame's origin, so a moving source leaves an even trail rather than clumps. */
|
|
115
138
|
#lastOrigin = /** @type {Vec3 | null} */ (null)
|
|
116
139
|
#particle = /** @type {Particle} */ ({ position: new Vec3(), velocity: new Vec3(), age: 0, lifetime: 0 })
|
|
@@ -144,12 +167,12 @@ export class Emitter extends Points {
|
|
|
144
167
|
/**
|
|
145
168
|
* Change several settings at once - a preset, say. Vectors are copied into the emitter's own,
|
|
146
169
|
* so anything bound to `emitter.gravity` and friends stays bound. Live particles carry on.
|
|
147
|
-
* @param {SpawnOptions & { rate?: number, gravity?: Vec3Input, drag?: number, follow?: Node | null,
|
|
170
|
+
* @param {SpawnOptions & LookOptions & { rate?: number, gravity?: Vec3Input, drag?: number, follow?: Node | null,
|
|
148
171
|
* onParticle?: ((p: Particle) => boolean | void) | null }} options
|
|
149
172
|
* @returns {this}
|
|
150
173
|
*/
|
|
151
174
|
set(options) {
|
|
152
|
-
const known = [...SPAWN_OPTIONS,
|
|
175
|
+
const known = [...SPAWN_OPTIONS, ...BEHAVIOUR_OPTIONS]
|
|
153
176
|
for (const key of Object.keys(options ?? {})) {
|
|
154
177
|
if (!known.includes(key)) fail('Emitter', `set() doesn't take '${key}' - it takes ${known.join(', ')}`)
|
|
155
178
|
}
|
|
@@ -169,6 +192,22 @@ export class Emitter extends Points {
|
|
|
169
192
|
/** @returns {[number, number, number][]} */
|
|
170
193
|
get color() { return /** @type {[number, number, number][]} */ (this.#palettes[0].map((s) => [...s])) }
|
|
171
194
|
|
|
195
|
+
/** Each particle's size over its life, as multiples of the material's. @param {number | number[]} v */
|
|
196
|
+
set size(v) { this.#setLooks({ size: v }) }
|
|
197
|
+
/** @returns {number[]} */
|
|
198
|
+
get size() { return [...this.#size] }
|
|
199
|
+
/** How much each particle's size varies, 0-1 either way. @param {number} v */
|
|
200
|
+
set sizeSpread(v) { this.#setLooks({ sizeSpread: v }) }
|
|
201
|
+
get sizeSpread() { return this.#sizeSpread }
|
|
202
|
+
/** Each particle's opacity over its life. @param {number | number[]} v */
|
|
203
|
+
set opacity(v) { this.#setLooks({ opacity: v }) }
|
|
204
|
+
/** @returns {number[]} */
|
|
205
|
+
get opacity() { return [...this.#opacity] }
|
|
206
|
+
/** How fast particles turn, radians per second, as a [min, max] range. @param {number | [number, number]} v */
|
|
207
|
+
set spin(v) { this.#setLooks({ spin: v }) }
|
|
208
|
+
/** @returns {[number, number]} */
|
|
209
|
+
get spin() { return [...this.#spin] }
|
|
210
|
+
|
|
172
211
|
/**
|
|
173
212
|
* Birth `count` particles at once - explosions, splashes. Options override the emitter's own
|
|
174
213
|
* spawn settings for this burst only.
|
|
@@ -211,6 +250,7 @@ export class Emitter extends Points {
|
|
|
211
250
|
const v = this.#velocities, ages = this.#ages, lives = this.#lifetimes
|
|
212
251
|
const g = this.gravity, keep = Math.exp(-this.drag * dt)
|
|
213
252
|
const hook = this.onParticle, particle = this.#particle
|
|
253
|
+
const angles = this.geometry.angles, spins = this.#spins
|
|
214
254
|
|
|
215
255
|
for (let i = 0; i < this.#alive;) {
|
|
216
256
|
const age = ages[i] += dt
|
|
@@ -223,6 +263,7 @@ export class Emitter extends Points {
|
|
|
223
263
|
v[i3 + k] = (v[i3 + k] + g[k] * dt) * keep
|
|
224
264
|
p[i3 + k] += v[i3 + k] * dt
|
|
225
265
|
}
|
|
266
|
+
if (angles && spins) angles[i] += spins[i] * dt
|
|
226
267
|
this.#paint(i, age / lives[i])
|
|
227
268
|
if (hook) {
|
|
228
269
|
particle.position.set(p[i3], p[i3 + 1], p[i3 + 2])
|
|
@@ -256,7 +297,7 @@ export class Emitter extends Points {
|
|
|
256
297
|
const rest = super.cloneOptions()
|
|
257
298
|
delete rest.geometry
|
|
258
299
|
return { ...rest, ...this.#saveSpawn(), max: this.#max, rate: this.rate, gravity: this.gravity.clone(),
|
|
259
|
-
drag: this.drag, follow: this.follow, onParticle: this.onParticle, color: this.color }
|
|
300
|
+
drag: this.drag, follow: this.follow, onParticle: this.onParticle, color: this.color, ...this.#looks() }
|
|
260
301
|
}
|
|
261
302
|
|
|
262
303
|
/** @internal @returns {Record<string, unknown>} */
|
|
@@ -266,7 +307,8 @@ export class Emitter extends Points {
|
|
|
266
307
|
return { ...rest, max: this.#max, rate: this.rate, gravity: this.gravity.toArray(), drag: this.drag,
|
|
267
308
|
origin: this.origin.toArray(), originSpread: this.originSpread, velocity: this.velocity.toArray(),
|
|
268
309
|
velocitySpread: typeof this.velocitySpread === 'number' ? this.velocitySpread : this.velocitySpread.toArray(),
|
|
269
|
-
lifetime: [...this.lifetime], color: this.#palettes[0].map((s) => new Color().setSRGB(s[0], s[1], s[2]).toJSON())
|
|
310
|
+
lifetime: [...this.lifetime], color: this.#palettes[0].map((s) => new Color().setSRGB(s[0], s[1], s[2]).toJSON()),
|
|
311
|
+
...this.#looks() }
|
|
270
312
|
}
|
|
271
313
|
|
|
272
314
|
// birth one particle somewhere between a and b, already `head` seconds into its life
|
|
@@ -287,6 +329,12 @@ export class Emitter extends Points {
|
|
|
287
329
|
this.#lifetimes[i] = min + Math.random() * (max - min)
|
|
288
330
|
this.#ages[i] = head
|
|
289
331
|
this.#palette[i] = this.#spawnPalette
|
|
332
|
+
if (this.#sizeFactors) this.#sizeFactors[i] = 1 + (Math.random() * 2 - 1) * this.#sizeSpread
|
|
333
|
+
const angles = this.geometry.angles, spins = this.#spins
|
|
334
|
+
if (angles && spins) {
|
|
335
|
+
spins[i] = this.#spin[0] + Math.random() * (this.#spin[1] - this.#spin[0])
|
|
336
|
+
angles[i] = Math.random() * Math.PI * 2 + spins[i] * head
|
|
337
|
+
}
|
|
290
338
|
this.#paint(i, head / this.#lifetimes[i])
|
|
291
339
|
}
|
|
292
340
|
|
|
@@ -302,10 +350,16 @@ export class Emitter extends Points {
|
|
|
302
350
|
this.#ages[i] = this.#ages[last]
|
|
303
351
|
this.#lifetimes[i] = this.#lifetimes[last]
|
|
304
352
|
this.#palette[i] = this.#palette[last]
|
|
353
|
+
for (const a of [this.geometry.sizes, this.geometry.opacities, this.geometry.angles, this.#sizeFactors, this.#spins]) {
|
|
354
|
+
if (a) a[i] = a[last]
|
|
355
|
+
}
|
|
305
356
|
}
|
|
306
357
|
|
|
307
358
|
/** @param {number} i @param {number} t 0-1 through its life */
|
|
308
359
|
#paint(i, t) {
|
|
360
|
+
const { sizes, opacities } = this.geometry
|
|
361
|
+
if (sizes) sizes[i] = along(this.#size, t) * (this.#sizeFactors ? this.#sizeFactors[i] : 1)
|
|
362
|
+
if (opacities) opacities[i] = along(this.#opacity, t)
|
|
309
363
|
const c = /** @type {Float32Array} */ (this.geometry.colors), stops = this.#palettes[this.#palette[i]]
|
|
310
364
|
if (stops.length === 1) {
|
|
311
365
|
c.set(stops[0], i * 3)
|
|
@@ -349,8 +403,9 @@ export class Emitter extends Points {
|
|
|
349
403
|
if (o.color !== undefined) this.#palettes[0] = colorStops(o.color)
|
|
350
404
|
}
|
|
351
405
|
|
|
352
|
-
/** @param {{ rate?: number, gravity?: Vec3Input, drag?: number, follow?: Node | null, onParticle?: ((p: Particle) => boolean | void) | null }} o */
|
|
406
|
+
/** @param {LookOptions & { rate?: number, gravity?: Vec3Input, drag?: number, follow?: Node | null, onParticle?: ((p: Particle) => boolean | void) | null }} o */
|
|
353
407
|
#setBehaviour(o) {
|
|
408
|
+
this.#setLooks(o)
|
|
354
409
|
const owner = 'Emitter'
|
|
355
410
|
if (o.rate !== undefined) this.rate = Math.max(0, checkNumber(owner, 'rate', o.rate))
|
|
356
411
|
if (o.gravity !== undefined) this.gravity.copy(o.gravity, owner)
|
|
@@ -359,6 +414,31 @@ export class Emitter extends Points {
|
|
|
359
414
|
if (o.onParticle !== undefined) this.onParticle = o.onParticle
|
|
360
415
|
}
|
|
361
416
|
|
|
417
|
+
// Sizes, opacities and angles go on the geometry when first wanted (the points are then drawn as
|
|
418
|
+
// squares) and stay; live particles get the plain values until they are next painted.
|
|
419
|
+
/** @param {LookOptions} o */
|
|
420
|
+
#setLooks(o) {
|
|
421
|
+
const owner = 'Emitter', g = this.geometry, max = this.#max
|
|
422
|
+
if (o.size !== undefined) this.#size = lookStops('size', o.size, Infinity)
|
|
423
|
+
if (o.sizeSpread !== undefined) this.#sizeSpread = Math.min(Math.max(checkNumber(owner, 'sizeSpread', o.sizeSpread), 0), 1)
|
|
424
|
+
if (o.opacity !== undefined) this.#opacity = lookStops('opacity', o.opacity, 1)
|
|
425
|
+
if (o.spin !== undefined) this.#spin = spinRange(o.spin)
|
|
426
|
+
const plain = (/** @type {number[]} */ stops) => stops.length === 1 && stops[0] === 1
|
|
427
|
+
if (!g.sizes && (!plain(this.#size) || this.#sizeSpread)) g.sizes = new Float32Array(max).fill(1)
|
|
428
|
+
if (!this.#sizeFactors && this.#sizeSpread) this.#sizeFactors = new Float32Array(max).fill(1)
|
|
429
|
+
if (!g.opacities && !plain(this.#opacity)) g.opacities = new Float32Array(max).fill(1)
|
|
430
|
+
if (!g.angles && (this.#spin[0] || this.#spin[1])) {
|
|
431
|
+
g.angles = new Float32Array(max)
|
|
432
|
+
this.#spins = new Float32Array(max)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The looks, as options. @returns {LookOptions} */
|
|
437
|
+
#looks() {
|
|
438
|
+
return { size: unstop(this.#size), sizeSpread: this.#sizeSpread, opacity: unstop(this.#opacity),
|
|
439
|
+
spin: /** @type {[number, number]} */ ([...this.#spin]) }
|
|
440
|
+
}
|
|
441
|
+
|
|
362
442
|
/** The spawn settings other than colour, as options. @returns {SpawnOptions} */
|
|
363
443
|
#saveSpawn() {
|
|
364
444
|
return { origin: this.origin.clone(), originSpread: this.originSpread, velocity: this.velocity.clone(),
|
|
@@ -397,6 +477,37 @@ function lifetimeRange(v) {
|
|
|
397
477
|
return [Math.min(range[0], range[1]), Math.max(range[0], range[1])]
|
|
398
478
|
}
|
|
399
479
|
|
|
480
|
+
/** @param {string} name @param {number | number[]} v @param {number} most @returns {number[]} */
|
|
481
|
+
function lookStops(name, v, most) {
|
|
482
|
+
const list = Array.isArray(v) ? v : [v]
|
|
483
|
+
if (!list.length || !list.every((n) => typeof n === 'number' && Number.isFinite(n) && n >= 0)) {
|
|
484
|
+
fail('Emitter', `'${name}' must be a number of 0 or more, or a list of them a particle goes through, got ${describe(v)}`)
|
|
485
|
+
}
|
|
486
|
+
return list.map((n) => Math.min(n, most))
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** One stop as a number, several as a list. @param {number[]} stops @returns {number | number[]} */
|
|
490
|
+
function unstop(stops) {
|
|
491
|
+
return stops.length === 1 ? stops[0] : [...stops]
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** The value t (0-1) of the way through evenly spaced stops. @param {number[]} stops @param {number} t */
|
|
495
|
+
function along(stops, t) {
|
|
496
|
+
if (stops.length === 1) return stops[0]
|
|
497
|
+
const x = Math.min(Math.max(t, 0), 1) * (stops.length - 1)
|
|
498
|
+
const s = Math.min(Math.floor(x), stops.length - 2)
|
|
499
|
+
return stops[s] + (stops[s + 1] - stops[s]) * (x - s)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** @param {number | [number, number]} v @returns {[number, number]} */
|
|
503
|
+
function spinRange(v) {
|
|
504
|
+
const range = typeof v === 'number' ? [-Math.abs(v), Math.abs(v)] : v
|
|
505
|
+
if (!Array.isArray(range) || range.length !== 2 || !range.every((n) => typeof n === 'number' && Number.isFinite(n))) {
|
|
506
|
+
fail('Emitter', `'spin' must be radians per second, or a [min, max] range, got ${describe(v)}`)
|
|
507
|
+
}
|
|
508
|
+
return [Math.min(range[0], range[1]), Math.max(range[0], range[1])]
|
|
509
|
+
}
|
|
510
|
+
|
|
400
511
|
/** @param {ColorInput | ColorInput[]} v @returns {number[][]} */
|
|
401
512
|
function colorStops(v) {
|
|
402
513
|
// a single [r, g, b] array is one colour, not three
|
package/src/scene/drawables.js
CHANGED
|
@@ -466,20 +466,44 @@ export class InstancedMesh extends Mesh {
|
|
|
466
466
|
/** Each vertex drawn as a square point sprite: star fields, particles, scatter plots. */
|
|
467
467
|
export class Points extends Drawable {
|
|
468
468
|
static type = 'points'
|
|
469
|
-
static get OPTIONS() { return [...DRAWABLE_OPTIONS, 'labels'] }
|
|
469
|
+
static get OPTIONS() { return [...DRAWABLE_OPTIONS, 'labels', 'sort'] }
|
|
470
470
|
|
|
471
471
|
/** A label per point from its row of data - or a list of them, several per point. @type {ReturnType<typeof instanceLabels>} */
|
|
472
472
|
labels = null
|
|
473
|
+
/**
|
|
474
|
+
* Draw the points back to front, furthest first, each frame - for soft points that cover what is
|
|
475
|
+
* behind them (smoke, with blending 'normal'), which then layer the right way round. Drawn as squares.
|
|
476
|
+
*/
|
|
477
|
+
sort = false
|
|
473
478
|
|
|
474
|
-
/** @param {DrawableOptions & { labels?: InstanceLabels | InstanceLabels[] | null }} options */
|
|
479
|
+
/** @param {DrawableOptions & { labels?: InstanceLabels | InstanceLabels[] | null, sort?: boolean }} options */
|
|
475
480
|
constructor(options) {
|
|
476
481
|
super(options)
|
|
477
482
|
this.labels = instanceLabels(this.constructor.name, options?.labels)
|
|
483
|
+
if (options?.sort !== undefined) {
|
|
484
|
+
if (typeof options.sort !== 'boolean') fail(this.constructor.name, `'sort' is true or false, got ${describe(options.sort)}`)
|
|
485
|
+
this.sort = options.sort
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* @internal Drawn as squares, one a point, rather than as GL points: when the geometry gives each
|
|
491
|
+
* point its own size, opacity or angle, or the points are sorted. Squares can be any size and stay
|
|
492
|
+
* whole at the edge of the view.
|
|
493
|
+
*/
|
|
494
|
+
get drawsSprites() {
|
|
495
|
+
const g = this.geometry
|
|
496
|
+
return this.sort || !!(g.sizes || g.opacities || g.angles)
|
|
478
497
|
}
|
|
479
498
|
|
|
480
499
|
/** @internal @returns {Record<string, unknown>} */
|
|
481
500
|
cloneOptions() {
|
|
482
|
-
return { ...super.cloneOptions(), labels: this.labels }
|
|
501
|
+
return { ...super.cloneOptions(), labels: this.labels, sort: this.sort }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** @internal @returns {Record<string, unknown>} */
|
|
505
|
+
jsonOptions() {
|
|
506
|
+
return { ...super.jsonOptions(), ...(this.sort ? { sort: true } : {}) }
|
|
483
507
|
}
|
|
484
508
|
|
|
485
509
|
/** @returns {'points'} */
|
|
@@ -488,12 +512,52 @@ export class Points extends Drawable {
|
|
|
488
512
|
}
|
|
489
513
|
}
|
|
490
514
|
|
|
515
|
+
const ARROWS = ['none', 'start', 'end', 'both']
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* @typedef {DrawableOptions & { arrows?: 'none' | 'start' | 'end' | 'both', arrowSize?: number }} LinesOptions
|
|
519
|
+
*/
|
|
520
|
+
|
|
491
521
|
/**
|
|
492
522
|
* Line segments: indices (or consecutive vertices) in pairs. Width is in pixels and works on
|
|
493
|
-
* every platform. edges(geometry) makes a wireframe of any geometry.
|
|
523
|
+
* every platform. edges(geometry) makes a wireframe of any geometry. Segments each starting where
|
|
524
|
+
* the one before ended make a chain - a polyline - which dashes run on along and arrowheads finish.
|
|
494
525
|
*/
|
|
495
526
|
export class Lines extends Drawable {
|
|
496
527
|
static type = 'lines'
|
|
528
|
+
static get OPTIONS() { return [...DRAWABLE_OPTIONS, 'arrows', 'arrowSize'] }
|
|
529
|
+
|
|
530
|
+
/** Arrowheads at the ends of each chain: at its 'start', its 'end', 'both' or 'none'. @type {'none' | 'start' | 'end' | 'both'} */
|
|
531
|
+
arrows = 'none'
|
|
532
|
+
/** Arrowhead length in pixels; 0 sizes them from the material's width. */
|
|
533
|
+
arrowSize = 0
|
|
534
|
+
|
|
535
|
+
/** @param {LinesOptions} options */
|
|
536
|
+
constructor(options) {
|
|
537
|
+
super(options)
|
|
538
|
+
if (options?.arrows !== undefined) this.arrows = /** @type {any} */ (checkOneOf('Lines', 'arrows', options.arrows, ARROWS))
|
|
539
|
+
if (options?.arrowSize !== undefined) this.arrowSize = Math.max(0, checkNumber('Lines', 'arrowSize', options.arrowSize))
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** @internal Which ends get arrowheads, for the shader: 1 the start, 2 the end. */
|
|
543
|
+
get arrowBits() {
|
|
544
|
+
return ARROWS.indexOf(this.arrows)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** @internal An arrowhead's length in pixels, before the pixel ratio. @param {number} width */
|
|
548
|
+
arrowPixels(width) {
|
|
549
|
+
return this.arrowSize || 6 + width * 3
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** @internal @returns {Record<string, unknown>} */
|
|
553
|
+
cloneOptions() {
|
|
554
|
+
return { ...super.cloneOptions(), arrows: this.arrows, arrowSize: this.arrowSize }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** @internal @returns {Record<string, unknown>} */
|
|
558
|
+
jsonOptions() {
|
|
559
|
+
return { ...super.jsonOptions(), ...(this.arrows !== 'none' ? { arrows: this.arrows } : {}), ...(this.arrowSize ? { arrowSize: this.arrowSize } : {}) }
|
|
560
|
+
}
|
|
497
561
|
|
|
498
562
|
/** @returns {'lines'} */
|
|
499
563
|
get drawMode() {
|
package/src/watch.js
CHANGED
|
@@ -147,7 +147,11 @@ export class ChangeWatch {
|
|
|
147
147
|
this.#push(g.version)
|
|
148
148
|
this.#push(g.drawCount ?? -1)
|
|
149
149
|
this.#push(node.renderOrder)
|
|
150
|
-
this.#push((node.castShadow ? 1 : 0) | (node.receiveShadow ? 2 : 0))
|
|
150
|
+
this.#push((node.castShadow ? 1 : 0) | (node.receiveShadow ? 2 : 0) | (node.sort ? 4 : 0))
|
|
151
|
+
if ('arrowBits' in node) {
|
|
152
|
+
this.#push(node.arrowBits)
|
|
153
|
+
this.#push(node.arrowSize)
|
|
154
|
+
}
|
|
151
155
|
// per-instance labels come from these, so swapping either redraws
|
|
152
156
|
this.#push(node.labels ? idOf(node.labels) : 0)
|
|
153
157
|
this.#push(node.data && typeof node.data === 'object' ? idOf(node.data) : 0)
|
|
@@ -164,6 +168,9 @@ export class ChangeWatch {
|
|
|
164
168
|
this.#push(m.metalness)
|
|
165
169
|
this.#push(m.size)
|
|
166
170
|
this.#push(m.width)
|
|
171
|
+
this.#push(m.dash)
|
|
172
|
+
this.#push(m.gap)
|
|
173
|
+
this.#push(m.dashOffset)
|
|
167
174
|
this.#push(m.alphaTest)
|
|
168
175
|
this.#push(m.normalScale)
|
|
169
176
|
this.#push(m.aoMapIntensity)
|
|
@@ -117,6 +117,8 @@ export class WebGLRenderer {
|
|
|
117
117
|
/** This frame's prefiltered environment, when it is an HDR image. @type {Environment | null} */ #environment = null
|
|
118
118
|
/** This frame's shadows - directional, spot, point - for the meshes that receive them. @type {import('./passes.js').ShadowMap[]} */ #shadows = []
|
|
119
119
|
/** @type {Float32Array} */ #normalMatrix = new Float32Array(9)
|
|
120
|
+
/** Model-view for sorting points back to front. */
|
|
121
|
+
#modelView = new Float32Array(16)
|
|
120
122
|
/** @type {Record<string, any>} */ #lights = {}
|
|
121
123
|
/** This frame waits for shaders to compile rather than leaving things out. */
|
|
122
124
|
#wait = false
|
|
@@ -407,6 +409,7 @@ export class WebGLRenderer {
|
|
|
407
409
|
|
|
408
410
|
const clip = kind === 'mesh' ? item.clip : null
|
|
409
411
|
const defines = this.#defines(kind, material, geometry.colors !== null, !!instances, geometry.uvs2 !== null)
|
|
412
|
+
if (kind === 'points' && /** @type {any} */ (node).drawsSprites) defines.push('SPRITES')
|
|
410
413
|
for (const shadow of shadows) defines.push(shadow.define)
|
|
411
414
|
const fog = material.fog ? frame.fog : null
|
|
412
415
|
if (fog) {
|
|
@@ -531,9 +534,18 @@ export class WebGLRenderer {
|
|
|
531
534
|
if (kind === 'lines') {
|
|
532
535
|
gl.disable(gl.CULL_FACE)
|
|
533
536
|
program.set('uWidth', material.width * this.pixelRatio)
|
|
537
|
+
const arrows = /** @type {any} */ (node).arrowBits ?? 0
|
|
538
|
+
program.set('uArrows', arrows)
|
|
539
|
+
program.set('uArrowSize', /** @type {any} */ (node).arrowPixels?.(material.width) * this.pixelRatio || 0)
|
|
540
|
+
if (material.dash > 0) {
|
|
541
|
+
program.set('uDash', material.dash)
|
|
542
|
+
program.set('uGap', material.gap || material.dash)
|
|
543
|
+
program.set('uDashOffset', material.dashOffset)
|
|
544
|
+
}
|
|
534
545
|
const buffers = this.#gpu.resources.lines(geometry)
|
|
535
546
|
gl.bindVertexArray(buffers.vao)
|
|
536
|
-
|
|
547
|
+
// a quad a segment, and two arrowheads where there are any
|
|
548
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, arrows ? 12 : 6, Math.min(buffers.count, Math.floor(geometry.drawnCount / 2)))
|
|
537
549
|
this.info.drawCalls++
|
|
538
550
|
return
|
|
539
551
|
}
|
|
@@ -541,9 +553,27 @@ export class WebGLRenderer {
|
|
|
541
553
|
if (kind === 'points') {
|
|
542
554
|
gl.disable(gl.CULL_FACE)
|
|
543
555
|
program.set('uSize', material.sizeAttenuation ? material.size : material.size * this.pixelRatio)
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
556
|
+
// round points are solid, but their edges are soft: blended over what is behind even in the
|
|
557
|
+
// opaque pass, or the edge fades to black - a dark ring round every dot
|
|
558
|
+
const round = !transparent && !material.map
|
|
559
|
+
if (round) {
|
|
560
|
+
gl.enable(gl.BLEND)
|
|
561
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
|
|
562
|
+
}
|
|
563
|
+
if (/** @type {any} */ (node).drawsSprites) {
|
|
564
|
+
// each point a square, any size and turned - drawn back to front when the points ask, for
|
|
565
|
+
// smoke and other soft things that hide what is behind them
|
|
566
|
+
program.set('uResolution', [this.canvas.width, this.canvas.height])
|
|
567
|
+
const modelView = /** @type {any} */ (node).sort ? mat4.multiply(this.#modelView, frame.camera.view, worldMatrix) : null
|
|
568
|
+
const buffers = this.#gpu.resources.sprites(geometry, modelView)
|
|
569
|
+
gl.bindVertexArray(buffers.vao)
|
|
570
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, buffers.count)
|
|
571
|
+
} else {
|
|
572
|
+
const buffers = this.#gpu.resources.mesh(geometry, null, false)
|
|
573
|
+
gl.bindVertexArray(buffers.vao)
|
|
574
|
+
gl.drawArrays(gl.POINTS, 0, geometry.drawnVertices)
|
|
575
|
+
}
|
|
576
|
+
if (round) gl.disable(gl.BLEND)
|
|
547
577
|
this.info.drawCalls++
|
|
548
578
|
return
|
|
549
579
|
}
|
|
@@ -676,7 +706,8 @@ export class WebGLRenderer {
|
|
|
676
706
|
} else if (kind === 'points') {
|
|
677
707
|
if (material.map) d.push('MAP')
|
|
678
708
|
if (material.sizeAttenuation) d.push('SIZE_ATTENUATION')
|
|
679
|
-
}
|
|
709
|
+
} else if (material.dash > 0) d.push('DASHED')
|
|
710
|
+
// (points drawn as squares add SPRITES - see #draw, which knows the node)
|
|
680
711
|
return d
|
|
681
712
|
}
|
|
682
713
|
}
|
package/src/webgl/picking.js
CHANGED
|
@@ -18,6 +18,9 @@ const PICK_FRAGMENT = /* glsl */ `
|
|
|
18
18
|
precision highp float;
|
|
19
19
|
precision highp int;
|
|
20
20
|
flat in uint vPickElement;
|
|
21
|
+
#ifdef SPRITES
|
|
22
|
+
in vec2 vUv;
|
|
23
|
+
#endif
|
|
21
24
|
#ifdef CLIP
|
|
22
25
|
in vec3 vWorldPosition;
|
|
23
26
|
#endif
|
|
@@ -30,7 +33,11 @@ void main() {
|
|
|
30
33
|
clipAway(vWorldPosition);
|
|
31
34
|
#endif
|
|
32
35
|
#ifdef ROUND
|
|
36
|
+
#ifdef SPRITES
|
|
37
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
38
|
+
#else
|
|
33
39
|
vec2 p = gl_PointCoord * 2.0 - 1.0;
|
|
40
|
+
#endif
|
|
34
41
|
if (dot(p, p) > 1.0) discard;
|
|
35
42
|
#endif
|
|
36
43
|
pickOut = uvec4(uPickId, vPickElement, floatBitsToUint(gl_FragCoord.z), 1u);
|
|
@@ -219,9 +226,11 @@ export class Picker {
|
|
|
219
226
|
const kind = mode === 'points' ? 'points' : mode === 'lines' ? 'lines' : 'mesh'
|
|
220
227
|
const defines = ['PICK']
|
|
221
228
|
if (instances) defines.push('INSTANCED')
|
|
229
|
+
const sprites = kind === 'points' && /** @type {any} */ (node).drawsSprites
|
|
222
230
|
if (kind === 'points') {
|
|
223
231
|
if (material.sizeAttenuation) defines.push('SIZE_ATTENUATION')
|
|
224
232
|
if (!material.map) defines.push('ROUND')
|
|
233
|
+
if (sprites) defines.push('SPRITES')
|
|
225
234
|
}
|
|
226
235
|
if (kind === 'mesh' && item.clip) defines.push('CLIP')
|
|
227
236
|
const program = this.programs.ready(`pick-${kind}`, defines, SOURCES[kind])
|
|
@@ -240,9 +249,13 @@ export class Picker {
|
|
|
240
249
|
program.set('uResolution', [REGION, REGION])
|
|
241
250
|
program.set('uWidth', material.width * pixelRatio)
|
|
242
251
|
program.set('uNear', frame.camera.near)
|
|
252
|
+
// arrowheads pick as their segment; dashes' gaps do too, so a dashed link is as easy to hit
|
|
253
|
+
const arrows = /** @type {any} */ (node).arrowBits ?? 0
|
|
254
|
+
program.set('uArrows', arrows)
|
|
255
|
+
program.set('uArrowSize', /** @type {any} */ (node).arrowPixels?.(material.width) * pixelRatio || 0)
|
|
243
256
|
const buffers = this.resources.lines(geometry)
|
|
244
257
|
gl.bindVertexArray(buffers.vao)
|
|
245
|
-
gl.drawArraysInstanced(gl.
|
|
258
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, arrows ? 12 : 6, Math.min(buffers.count, Math.floor(geometry.drawnCount / 2)))
|
|
246
259
|
return
|
|
247
260
|
}
|
|
248
261
|
if (kind === 'points') {
|
|
@@ -250,6 +263,14 @@ export class Picker {
|
|
|
250
263
|
// sizes as drawn: world sizes scale by the real projection, not the zoomed one
|
|
251
264
|
program.set('uSize', material.sizeAttenuation ? material.size : material.size * pixelRatio)
|
|
252
265
|
program.set('uScale', frame.camera.projection[5] * height / 2)
|
|
266
|
+
if (sprites) {
|
|
267
|
+
// squares, in whatever order they were last drawn - each says which point it is
|
|
268
|
+
program.set('uResolution', [REGION, REGION])
|
|
269
|
+
const buffers = this.resources.sprites(geometry)
|
|
270
|
+
gl.bindVertexArray(buffers.vao)
|
|
271
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, buffers.count)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
253
274
|
const buffers = this.resources.mesh(geometry, null, false)
|
|
254
275
|
gl.bindVertexArray(buffers.vao)
|
|
255
276
|
gl.drawArrays(gl.POINTS, 0, geometry.drawnVertices)
|