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