-
Notifications
You must be signed in to change notification settings - Fork 139
/
Copy pathDerivedMaterial.js
426 lines (381 loc) · 16.7 KB
/
DerivedMaterial.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
import { voidMainRegExp } from './voidMainRegExp.js'
import { expandShaderIncludes } from './expandShaderIncludes.js'
import { MeshDepthMaterial, MeshDistanceMaterial, RGBADepthPacking, UniformsUtils } from 'three'
import { generateUUID } from './generateUUID.js'
// Local assign polyfill to avoid importing troika-core
const assign = Object.assign || function(/*target, ...sources*/) {
let target = arguments[0]
for (let i = 1, len = arguments.length; i < len; i++) {
let source = arguments[i]
if (source) {
for (let prop in source) {
if (Object.prototype.hasOwnProperty.call(source, prop)) {
target[prop] = source[prop]
}
}
}
}
return target
}
const epoch = Date.now()
const CONSTRUCTOR_CACHE = new WeakMap()
const SHADER_UPGRADE_CACHE = new Map()
// Material ids must be integers, but we can't access the increment from Three's `Material` module,
// so let's choose a sufficiently large starting value that should theoretically never collide.
let materialInstanceId = 1e10
/**
* A utility for creating a custom shader material derived from another material's
* shaders. This allows you to inject custom shader logic and transforms into the
* builtin ThreeJS materials without having to recreate them from scratch.
*
* @param {THREE.Material} baseMaterial - the original material to derive from
*
* @param {Object} options - How the base material should be modified.
* @param {Object=} options.defines - Custom `defines` for the material
* @param {Object=} options.extensions - Custom `extensions` for the material, e.g. `{derivatives: true}`
* @param {Object=} options.uniforms - Custom `uniforms` for use in the modified shader. These can
* be accessed and manipulated via the resulting material's `uniforms` property, just like
* in a ShaderMaterial. You do not need to repeat the base material's own uniforms here.
* @param {String=} options.timeUniform - If specified, a uniform of this name will be injected into
* both shaders, and it will automatically be updated on each render frame with a number of
* elapsed milliseconds. The "zero" epoch time is not significant so don't rely on this as a
* true calendar time.
* @param {String=} options.vertexDefs - Custom GLSL code to inject into the vertex shader's top-level
* definitions, above the `void main()` function.
* @param {String=} options.vertexMainIntro - Custom GLSL code to inject at the top of the vertex
* shader's `void main` function.
* @param {String=} options.vertexMainOutro - Custom GLSL code to inject at the end of the vertex
* shader's `void main` function.
* @param {String=} options.vertexTransform - Custom GLSL code to manipulate the `position`, `normal`,
* and/or `uv` vertex attributes. This code will be wrapped within a standalone function with
* those attributes exposed by their normal names as read/write values.
* @param {String=} options.fragmentDefs - Custom GLSL code to inject into the fragment shader's top-level
* definitions, above the `void main()` function.
* @param {String=} options.fragmentMainIntro - Custom GLSL code to inject at the top of the fragment
* shader's `void main` function.
* @param {String=} options.fragmentMainOutro - Custom GLSL code to inject at the end of the fragment
* shader's `void main` function. You can manipulate `gl_FragColor` here but keep in mind it goes
* after any of ThreeJS's color postprocessing shader chunks (tonemapping, fog, etc.), so if you
* want those to apply to your changes use `fragmentColorTransform` instead.
* @param {String=} options.fragmentColorTransform - Custom GLSL code to manipulate the `gl_FragColor`
* output value. Will be injected near the end of the `void main` function, but before any
* of ThreeJS's color postprocessing shader chunks (tonemapping, fog, etc.), and before the
* `fragmentMainOutro`.
* @param {function({fragmentShader: string, vertexShader:string}):
* {fragmentShader: string, vertexShader:string}} options.customRewriter - A function
* for performing custom rewrites of the full shader code. Useful if you need to do something
* special that's not covered by the other builtin options. This function will be executed before
* any other transforms are applied.
* @param {boolean=} options.chained - Set to `true` to prototype-chain the derived material to the base
* material, rather than the default behavior of copying it. This allows the derived material to
* automatically pick up changes made to the base material and its properties. This can be useful
* where the derived material is hidden from the user as an implementation detail, allowing them
* to work with the original material like normal. But it can result in unexpected behavior if not
* handled carefully.
*
* @return {THREE.Material}
*
* The returned material will also have two new methods, `getDepthMaterial()` and `getDistanceMaterial()`,
* which can be called to get a variant of the derived material for use in shadow casting. If the
* target mesh is expected to cast shadows, then you can assign these to the mesh's `customDepthMaterial`
* (for directional and spot lights) and/or `customDistanceMaterial` (for point lights) properties to
* allow the cast shadow to honor your derived shader's vertex transforms and discarded fragments. These
* will also set a custom `#define IS_DEPTH_MATERIAL` or `#define IS_DISTANCE_MATERIAL` that you can look
* for in your derived shaders with `#ifdef` to customize their behavior for the depth or distance
* scenarios, e.g. skipping antialiasing or expensive shader logic.
*/
export function createDerivedMaterial(baseMaterial, options) {
// Generate a key that is unique to the content of these `options`. We'll use this
// throughout for caching and for generating the upgraded shader code. This increases
// the likelihood that the resulting shaders will line up across multiple calls so
// their GL programs can be shared and cached.
const optionsKey = getKeyForOptions(options)
// First check to see if we've already derived from this baseMaterial using this
// unique set of options, and if so reuse the constructor to avoid some allocations.
let ctorsByDerivation = CONSTRUCTOR_CACHE.get(baseMaterial)
if (!ctorsByDerivation) {
CONSTRUCTOR_CACHE.set(baseMaterial, (ctorsByDerivation = Object.create(null)))
}
if (ctorsByDerivation[optionsKey]) {
return new ctorsByDerivation[optionsKey]()
}
const privateBeforeCompileProp = `_onBeforeCompile${optionsKey}`
// Private onBeforeCompile handler that injects the modified shaders and uniforms when
// the renderer switches to this material's program
const onBeforeCompile = function (shaderInfo, renderer) {
baseMaterial.onBeforeCompile.call(this, shaderInfo, renderer)
// Upgrade the shaders, caching the result by incoming source code
const cacheKey = this.customProgramCacheKey() + '|' + shaderInfo.vertexShader + '|' + shaderInfo.fragmentShader
let upgradedShaders = SHADER_UPGRADE_CACHE[cacheKey]
if (!upgradedShaders) {
const upgraded = upgradeShaders(this, shaderInfo, options, optionsKey)
upgradedShaders = SHADER_UPGRADE_CACHE[cacheKey] = upgraded
}
// Inject upgraded shaders and uniforms into the program
shaderInfo.vertexShader = upgradedShaders.vertexShader
shaderInfo.fragmentShader = upgradedShaders.fragmentShader
assign(shaderInfo.uniforms, this.uniforms)
// Inject auto-updating time uniform if requested
if (options.timeUniform) {
shaderInfo.uniforms[options.timeUniform] = {
get value() {return Date.now() - epoch}
}
}
// Users can still add their own handlers on top of ours
if (this[privateBeforeCompileProp]) {
this[privateBeforeCompileProp](shaderInfo)
}
}
const DerivedMaterial = function DerivedMaterial() {
return derive(options.chained ? baseMaterial : baseMaterial.clone())
}
const derive = function(base) {
// Prototype chain to the base material
const derived = Object.create(base, descriptor)
// Store the baseMaterial for reference; this is always the original even when cloning
Object.defineProperty(derived, 'baseMaterial', { value: baseMaterial })
// Needs its own ids
Object.defineProperty(derived, 'id', { value: materialInstanceId++ })
derived.uuid = generateUUID()
// Merge uniforms, defines, and extensions
derived.uniforms = assign({}, base.uniforms, options.uniforms)
derived.defines = assign({}, base.defines, options.defines)
derived.defines[`TROIKA_DERIVED_MATERIAL_${optionsKey}`] = '' //force a program change from the base material
derived.extensions = assign({}, base.extensions, options.extensions)
// Don't inherit EventDispatcher listeners
derived._listeners = undefined
return derived
}
const descriptor = {
constructor: {value: DerivedMaterial},
isDerivedMaterial: {value: true},
type: {
get: () => baseMaterial.type,
set: (value) => {baseMaterial.type = value}
},
isDerivedFrom: {
writable: true,
configurable: true,
value: function (testMaterial) {
const base = this.baseMaterial
return testMaterial === base || (base.isDerivedMaterial && base.isDerivedFrom(testMaterial)) || false
}
},
customProgramCacheKey: {
writable: true,
configurable: true,
value: function () {
return baseMaterial.customProgramCacheKey() + '|' + optionsKey
}
},
onBeforeCompile: {
get() {
return onBeforeCompile
},
set(fn) {
this[privateBeforeCompileProp] = fn
}
},
copy: {
writable: true,
configurable: true,
value: function (source) {
baseMaterial.copy.call(this, source)
if (!baseMaterial.isShaderMaterial && !baseMaterial.isDerivedMaterial) {
assign(this.extensions, source.extensions)
assign(this.defines, source.defines)
assign(this.uniforms, UniformsUtils.clone(source.uniforms))
}
return this
}
},
clone: {
writable: true,
configurable: true,
value: function () {
const newBase = new baseMaterial.constructor()
return derive(newBase).copy(this)
}
},
/**
* Utility to get a MeshDepthMaterial that will honor this derived material's vertex
* transformations and discarded fragments.
*/
getDepthMaterial: {
writable: true,
configurable: true,
value: function() {
let depthMaterial = this._depthMaterial
if (!depthMaterial) {
depthMaterial = this._depthMaterial = createDerivedMaterial(
baseMaterial.isDerivedMaterial
? baseMaterial.getDepthMaterial()
: new MeshDepthMaterial({ depthPacking: RGBADepthPacking }),
options
)
depthMaterial.defines.IS_DEPTH_MATERIAL = ''
depthMaterial.uniforms = this.uniforms //automatically recieve same uniform values
}
return depthMaterial
}
},
/**
* Utility to get a MeshDistanceMaterial that will honor this derived material's vertex
* transformations and discarded fragments.
*/
getDistanceMaterial: {
writable: true,
configurable: true,
value: function() {
let distanceMaterial = this._distanceMaterial
if (!distanceMaterial) {
distanceMaterial = this._distanceMaterial = createDerivedMaterial(
baseMaterial.isDerivedMaterial
? baseMaterial.getDistanceMaterial()
: new MeshDistanceMaterial(),
options
)
distanceMaterial.defines.IS_DISTANCE_MATERIAL = ''
distanceMaterial.uniforms = this.uniforms //automatically recieve same uniform values
}
return distanceMaterial
}
},
dispose: {
writable: true,
configurable: true,
value() {
const {_depthMaterial, _distanceMaterial} = this
if (_depthMaterial) _depthMaterial.dispose()
if (_distanceMaterial) _distanceMaterial.dispose()
baseMaterial.dispose.call(this)
}
}
}
ctorsByDerivation[optionsKey] = DerivedMaterial
return new DerivedMaterial()
}
function upgradeShaders(material, {vertexShader, fragmentShader}, options, key) {
let {
vertexDefs,
vertexMainIntro,
vertexMainOutro,
vertexTransform,
fragmentDefs,
fragmentMainIntro,
fragmentMainOutro,
fragmentColorTransform,
customRewriter,
timeUniform
} = options
vertexDefs = vertexDefs || ''
vertexMainIntro = vertexMainIntro || ''
vertexMainOutro = vertexMainOutro || ''
fragmentDefs = fragmentDefs || ''
fragmentMainIntro = fragmentMainIntro || ''
fragmentMainOutro = fragmentMainOutro || ''
// Expand includes if needed
if (vertexTransform || customRewriter) {
vertexShader = expandShaderIncludes(vertexShader)
}
if (fragmentColorTransform || customRewriter) {
// We need to be able to find postprocessing chunks after include expansion in order to
// put them after the fragmentColorTransform, so mark them with comments first. Even if
// this particular derivation doesn't have a fragmentColorTransform, other derivations may,
// so we still mark them.
fragmentShader = fragmentShader.replace(
/^[ \t]*#include <((?:tonemapping|encodings|colorspace|fog|premultiplied_alpha|dithering)_fragment)>/gm,
'\n//!BEGIN_POST_CHUNK $1\n$&\n//!END_POST_CHUNK\n'
)
fragmentShader = expandShaderIncludes(fragmentShader)
}
// Apply custom rewriter function
if (customRewriter) {
let res = customRewriter({vertexShader, fragmentShader})
vertexShader = res.vertexShader
fragmentShader = res.fragmentShader
}
// The fragmentColorTransform needs to go before any postprocessing chunks, so extract
// those and re-insert them into the outro in the correct place:
if (fragmentColorTransform) {
let postChunks = []
fragmentShader = fragmentShader.replace(
/^\/\/!BEGIN_POST_CHUNK[^]+?^\/\/!END_POST_CHUNK/gm, // [^]+? = non-greedy match of any chars including newlines
match => {
postChunks.push(match)
return ''
}
)
fragmentMainOutro = `${fragmentColorTransform}\n${postChunks.join('\n')}\n${fragmentMainOutro}`
}
// Inject auto-updating time uniform if requested
if (timeUniform) {
const code = `\nuniform float ${timeUniform};\n`
vertexDefs = code + vertexDefs
fragmentDefs = code + fragmentDefs
}
// Inject a function for the vertexTransform and rename all usages of position/normal/uv
if (vertexTransform) {
// Hoist these defs to the very top so they work in other function defs
vertexShader = `vec3 troika_position_${key};
vec3 troika_normal_${key};
vec2 troika_uv_${key};
${vertexShader}
`
vertexDefs = `${vertexDefs}
void troikaVertexTransform${key}(inout vec3 position, inout vec3 normal, inout vec2 uv) {
${vertexTransform}
}
`
vertexMainIntro = `
troika_position_${key} = vec3(position);
troika_normal_${key} = vec3(normal);
troika_uv_${key} = vec2(uv);
troikaVertexTransform${key}(troika_position_${key}, troika_normal_${key}, troika_uv_${key});
${vertexMainIntro}
`
vertexShader = vertexShader.replace(/\b(position|normal|uv)\b/g, (match, match1, index, fullStr) => {
return /\battribute\s+vec[23]\s+$/.test(fullStr.substr(0, index)) ? match1 : `troika_${match1}_${key}`
})
// Three r152 introduced the MAP_UV token, replace it too if it's pointing to the main 'uv'
// Perhaps the other textures too going forward?
if (!(material.map && material.map.channel > 0)) {
vertexShader = vertexShader.replace(/\bMAP_UV\b/g, `troika_uv_${key}`);
}
}
// Inject defs and intro/outro snippets
vertexShader = injectIntoShaderCode(vertexShader, key, vertexDefs, vertexMainIntro, vertexMainOutro)
fragmentShader = injectIntoShaderCode(fragmentShader, key, fragmentDefs, fragmentMainIntro, fragmentMainOutro)
return {
vertexShader,
fragmentShader
}
}
function injectIntoShaderCode(shaderCode, id, defs, intro, outro) {
if (intro || outro || defs) {
shaderCode = shaderCode.replace(voidMainRegExp, `
${defs}
void troikaOrigMain${id}() {`
)
shaderCode += `
void main() {
${intro}
troikaOrigMain${id}();
${outro}
}`
}
return shaderCode
}
function optionsJsonReplacer(key, value) {
return key === 'uniforms' ? undefined : typeof value === 'function' ? value.toString() : value
}
let _idCtr = 0
const optionsHashesToIds = new Map()
function getKeyForOptions(options) {
const optionsHash = JSON.stringify(options, optionsJsonReplacer)
let id = optionsHashesToIds.get(optionsHash)
if (id == null) {
optionsHashesToIds.set(optionsHash, (id = ++_idCtr))
}
return id
}