forked from sagnik/Project_Velocity
427 lines
17 KiB
JavaScript
427 lines
17 KiB
JavaScript
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
|
|
}
|