feat(All):Initial
This commit is contained in:
@@ -0,0 +1,728 @@
|
||||
import { ShaderChunk, UniformsUtils, MeshDepthMaterial, RGBADepthPacking, MeshDistanceMaterial, ShaderLib, Matrix4, Vector3, Mesh, CylinderGeometry as CylinderBufferGeometry, Vector2, MeshStandardMaterial, DoubleSide } from 'three';
|
||||
|
||||
/**
|
||||
* Regular expression for matching the `void main() {` opener line in GLSL.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
const voidMainRegExp = /\bvoid\s+main\s*\(\s*\)\s*{/g;
|
||||
|
||||
/**
|
||||
* Recursively expands all `#include <xyz>` statements within string of shader code.
|
||||
* Copied from three's WebGLProgram#parseIncludes for external use.
|
||||
*
|
||||
* @param {string} source - The GLSL source code to evaluate
|
||||
* @return {string} The GLSL code with all includes expanded
|
||||
*/
|
||||
function expandShaderIncludes( source ) {
|
||||
const pattern = /^[ \t]*#include +<([\w\d./]+)>/gm;
|
||||
function replace(match, include) {
|
||||
let chunk = ShaderChunk[include];
|
||||
return chunk ? expandShaderIncludes(chunk) : match
|
||||
}
|
||||
return source.replace( pattern, replace )
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a direct copy of MathUtils.generateUUID from Three.js, to preserve compatibility with three
|
||||
* versions before 0.113.0 as it was changed from Math to MathUtils in that version.
|
||||
* https://github.com/mrdoob/three.js/blob/dd8b5aa3b270c17096b90945cd2d6d1b13aaec53/src/math/MathUtils.js#L16
|
||||
*/
|
||||
|
||||
const _lut = [];
|
||||
|
||||
for (let i = 0; i < 256; i++) {
|
||||
_lut[i] = (i < 16 ? '0' : '') + (i).toString(16);
|
||||
}
|
||||
|
||||
function generateUUID() {
|
||||
|
||||
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136
|
||||
|
||||
const d0 = Math.random() * 0xffffffff | 0;
|
||||
const d1 = Math.random() * 0xffffffff | 0;
|
||||
const d2 = Math.random() * 0xffffffff | 0;
|
||||
const d3 = Math.random() * 0xffffffff | 0;
|
||||
const uuid = _lut[d0 & 0xff] + _lut[d0 >> 8 & 0xff] + _lut[d0 >> 16 & 0xff] + _lut[d0 >> 24 & 0xff] + '-' +
|
||||
_lut[d1 & 0xff] + _lut[d1 >> 8 & 0xff] + '-' + _lut[d1 >> 16 & 0x0f | 0x40] + _lut[d1 >> 24 & 0xff] + '-' +
|
||||
_lut[d2 & 0x3f | 0x80] + _lut[d2 >> 8 & 0xff] + '-' + _lut[d2 >> 16 & 0xff] + _lut[d2 >> 24 & 0xff] +
|
||||
_lut[d3 & 0xff] + _lut[d3 >> 8 & 0xff] + _lut[d3 >> 16 & 0xff] + _lut[d3 >> 24 & 0xff];
|
||||
|
||||
// .toUpperCase() here flattens concatenated strings to save heap memory space.
|
||||
return uuid.toUpperCase()
|
||||
|
||||
}
|
||||
|
||||
// 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 (source.hasOwnProperty(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<{vertexShader,fragmentShader}>:{vertexShader,fragmentShader}} 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.
|
||||
*/
|
||||
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) {
|
||||
baseMaterial.onBeforeCompile.call(this, shaderInfo);
|
||||
|
||||
// Upgrade the shaders, caching the result by incoming source code
|
||||
const cacheKey = optionsKey + '|||' + shaderInfo.vertexShader + '|||' + shaderInfo.fragmentShader;
|
||||
let upgradedShaders = SHADER_UPGRADE_CACHE[cacheKey];
|
||||
if (!upgradedShaders) {
|
||||
const upgraded = upgradeShaders(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},
|
||||
|
||||
customProgramCacheKey: {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: function () {
|
||||
return 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({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|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}`
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Copied from threejs WebGLPrograms.js so we can resolve builtin materials to their shaders
|
||||
// TODO how can we keep this from getting stale?
|
||||
const MATERIAL_TYPES_TO_SHADERS = {
|
||||
MeshDepthMaterial: 'depth',
|
||||
MeshDistanceMaterial: 'distanceRGBA',
|
||||
MeshNormalMaterial: 'normal',
|
||||
MeshBasicMaterial: 'basic',
|
||||
MeshLambertMaterial: 'lambert',
|
||||
MeshPhongMaterial: 'phong',
|
||||
MeshToonMaterial: 'toon',
|
||||
MeshStandardMaterial: 'physical',
|
||||
MeshPhysicalMaterial: 'physical',
|
||||
MeshMatcapMaterial: 'matcap',
|
||||
LineBasicMaterial: 'basic',
|
||||
LineDashedMaterial: 'dashed',
|
||||
PointsMaterial: 'points',
|
||||
ShadowMaterial: 'shadow',
|
||||
SpriteMaterial: 'sprite'
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a Three.js `Material` instance, find the shaders/uniforms that will be
|
||||
* used to render that material.
|
||||
*
|
||||
* @param material - the Material instance
|
||||
* @return {object} - the material's shader info: `{uniforms:{}, fragmentShader:'', vertexShader:''}`
|
||||
*/
|
||||
function getShadersForMaterial(material) {
|
||||
let builtinType = MATERIAL_TYPES_TO_SHADERS[material.type];
|
||||
return builtinType ? ShaderLib[builtinType] : material //TODO fallback for unknown type?
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all uniforms and their types within a shader code string.
|
||||
*
|
||||
* @param {string} shader - The shader code to parse
|
||||
* @return {object} mapping of uniform names to their glsl type
|
||||
*/
|
||||
function getShaderUniformTypes(shader) {
|
||||
let uniformRE = /\buniform\s+(int|float|vec[234])\s+([A-Za-z_][\w]*)/g;
|
||||
let uniforms = Object.create(null);
|
||||
let match;
|
||||
while ((match = uniformRE.exec(shader)) !== null) {
|
||||
uniforms[match[2]] = match[1];
|
||||
}
|
||||
return uniforms
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for smoothing out the `m.getInverse(x)` --> `m.copy(x).invert()` conversion
|
||||
* that happened in ThreeJS r123.
|
||||
* @param {Matrix4} srcMatrix
|
||||
* @param {Matrix4} [tgtMatrix]
|
||||
*/
|
||||
function invertMatrix4(srcMatrix, tgtMatrix = new Matrix4()) {
|
||||
if (typeof tgtMatrix.invert === 'function') {
|
||||
tgtMatrix.copy(srcMatrix).invert();
|
||||
} else {
|
||||
tgtMatrix.getInverse(srcMatrix);
|
||||
}
|
||||
return tgtMatrix
|
||||
}
|
||||
|
||||
/*
|
||||
Input geometry is a cylinder with r=1, height in y dimension from 0 to 1,
|
||||
divided into a reasonable number of height segments.
|
||||
*/
|
||||
|
||||
const vertexDefs = `
|
||||
uniform vec3 pointA;
|
||||
uniform vec3 controlA;
|
||||
uniform vec3 controlB;
|
||||
uniform vec3 pointB;
|
||||
uniform float radius;
|
||||
varying float bezierT;
|
||||
|
||||
vec3 cubicBezier(vec3 p1, vec3 c1, vec3 c2, vec3 p2, float t) {
|
||||
float t2 = 1.0 - t;
|
||||
float b0 = t2 * t2 * t2;
|
||||
float b1 = 3.0 * t * t2 * t2;
|
||||
float b2 = 3.0 * t * t * t2;
|
||||
float b3 = t * t * t;
|
||||
return b0 * p1 + b1 * c1 + b2 * c2 + b3 * p2;
|
||||
}
|
||||
|
||||
vec3 cubicBezierDerivative(vec3 p1, vec3 c1, vec3 c2, vec3 p2, float t) {
|
||||
float t2 = 1.0 - t;
|
||||
return -3.0 * p1 * t2 * t2 +
|
||||
c1 * (3.0 * t2 * t2 - 6.0 * t2 * t) +
|
||||
c2 * (6.0 * t2 * t - 3.0 * t * t) +
|
||||
3.0 * p2 * t * t;
|
||||
}
|
||||
`;
|
||||
|
||||
const vertexTransform = `
|
||||
float t = position.y;
|
||||
bezierT = t;
|
||||
vec3 bezierCenterPos = cubicBezier(pointA, controlA, controlB, pointB, t);
|
||||
vec3 bezierDir = normalize(cubicBezierDerivative(pointA, controlA, controlB, pointB, t));
|
||||
|
||||
// Make "sideways" always perpendicular to the camera ray; this ensures that any twists
|
||||
// in the cylinder occur where you won't see them:
|
||||
vec3 viewDirection = normalMatrix * vec3(0.0, 0.0, 1.0);
|
||||
if (bezierDir == viewDirection) {
|
||||
bezierDir = normalize(cubicBezierDerivative(pointA, controlA, controlB, pointB, t == 1.0 ? t - 0.0001 : t + 0.0001));
|
||||
}
|
||||
vec3 sideways = normalize(cross(bezierDir, viewDirection));
|
||||
vec3 upish = normalize(cross(sideways, bezierDir));
|
||||
|
||||
// Build a matrix for transforming this disc in the cylinder:
|
||||
mat4 discTx;
|
||||
discTx[0].xyz = sideways * radius;
|
||||
discTx[1].xyz = bezierDir * radius;
|
||||
discTx[2].xyz = upish * radius;
|
||||
discTx[3].xyz = bezierCenterPos;
|
||||
discTx[3][3] = 1.0;
|
||||
|
||||
// Apply transform, ignoring original y
|
||||
position = (discTx * vec4(position.x, 0.0, position.z, 1.0)).xyz;
|
||||
normal = normalize(mat3(discTx) * normal);
|
||||
`;
|
||||
|
||||
const fragmentDefs = `
|
||||
uniform vec3 dashing;
|
||||
varying float bezierT;
|
||||
`;
|
||||
|
||||
const fragmentMainIntro = `
|
||||
if (dashing.x + dashing.y > 0.0) {
|
||||
float dashFrac = mod(bezierT - dashing.z, dashing.x + dashing.y);
|
||||
if (dashFrac > dashing.x) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Debugging: separate color for each of the 6 sides:
|
||||
// const fragmentColorTransform = `
|
||||
// float sideNum = floor(vUV.x * 6.0);
|
||||
// vec3 mixColor = sideNum < 1.0 ? vec3(1.0, 0.0, 0.0) :
|
||||
// sideNum < 2.0 ? vec3(0.0, 1.0, 1.0) :
|
||||
// sideNum < 3.0 ? vec3(1.0, 1.0, 0.0) :
|
||||
// sideNum < 4.0 ? vec3(0.0, 0.0, 1.0) :
|
||||
// sideNum < 5.0 ? vec3(0.0, 1.0, 0.0) :
|
||||
// vec3(1.0, 0.0, 1.0);
|
||||
// gl_FragColor.xyz = mix(gl_FragColor.xyz, mixColor, 0.5);
|
||||
// `
|
||||
|
||||
|
||||
|
||||
function createBezierMeshMaterial(baseMaterial) {
|
||||
return createDerivedMaterial(
|
||||
baseMaterial,
|
||||
{
|
||||
chained: true,
|
||||
uniforms: {
|
||||
pointA: {value: new Vector3()},
|
||||
controlA: {value: new Vector3()},
|
||||
controlB: {value: new Vector3()},
|
||||
pointB: {value: new Vector3()},
|
||||
radius: {value: 0.01},
|
||||
dashing: {value: new Vector3()} //on, off, offset
|
||||
},
|
||||
vertexDefs,
|
||||
vertexTransform,
|
||||
fragmentDefs,
|
||||
fragmentMainIntro
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let geometry = null;
|
||||
|
||||
const defaultBaseMaterial = /*#__PURE__*/new MeshStandardMaterial({color: 0xffffff, side: DoubleSide});
|
||||
|
||||
|
||||
/**
|
||||
* A ThreeJS `Mesh` that bends a tube shape along a 3D cubic bezier path. The bending is done
|
||||
* by deforming a straight cylindrical geometry in the vertex shader based on a set of four
|
||||
* control point uniforms. It patches the necessary GLSL into the mesh's assigned `material`
|
||||
* automatically.
|
||||
*
|
||||
* The cubiz bezier path is determined by its four `Vector3` properties:
|
||||
* - `pointA`
|
||||
* - `controlA`
|
||||
* - `controlB`
|
||||
* - `pointB`
|
||||
*
|
||||
* The tube's radius is controlled by its `radius` property, which defaults to `0.01`.
|
||||
*
|
||||
* You can also give the tube a dashed appearance with two properties:
|
||||
*
|
||||
* - `dashArray` - an array of two numbers, defining the length of "on" and "off" parts of
|
||||
* the dash. Each is a 0-1 ratio of the entire path's length. (Actually this is the `t` length
|
||||
* used as input to the cubic bezier function, not its visible length.)
|
||||
* - `dashOffset` - offset of where the dash starts. You can animate this to make the dashes move.
|
||||
*
|
||||
* Note that the dashes will appear like a hollow tube, not solid. This will be more apparent on
|
||||
* thicker tubes.
|
||||
*
|
||||
* TODO: proper geometry bounding sphere and raycasting
|
||||
* TODO: allow control of the geometry's segment counts
|
||||
*/
|
||||
class BezierMesh extends Mesh {
|
||||
static getGeometry() {
|
||||
return geometry || (geometry =
|
||||
new CylinderBufferGeometry(1, 1, 1, 6, 64).translate(0, 0.5, 0)
|
||||
)
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
BezierMesh.getGeometry(),
|
||||
defaultBaseMaterial
|
||||
);
|
||||
|
||||
this.pointA = new Vector3();
|
||||
this.controlA = new Vector3();
|
||||
this.controlB = new Vector3();
|
||||
this.pointB = new Vector3();
|
||||
this.radius = 0.01;
|
||||
this.dashArray = new Vector2();
|
||||
this.dashOffset = 0;
|
||||
|
||||
// TODO - disabling frustum culling until I figure out how to customize the
|
||||
// geometry's bounding sphere that gets used
|
||||
this.frustumCulled = false;
|
||||
}
|
||||
|
||||
// Handler for automatically wrapping the base material with our upgrades. We do the wrapping
|
||||
// lazily on _read_ rather than write to avoid unnecessary wrapping on transient values.
|
||||
get material() {
|
||||
let derivedMaterial = this._derivedMaterial;
|
||||
const baseMaterial = this._baseMaterial || this._defaultMaterial || (this._defaultMaterial = defaultBaseMaterial.clone());
|
||||
if (!derivedMaterial || derivedMaterial.baseMaterial !== baseMaterial) {
|
||||
derivedMaterial = this._derivedMaterial = createBezierMeshMaterial(baseMaterial);
|
||||
// dispose the derived material when its base material is disposed:
|
||||
baseMaterial.addEventListener('dispose', function onDispose() {
|
||||
baseMaterial.removeEventListener('dispose', onDispose);
|
||||
derivedMaterial.dispose();
|
||||
});
|
||||
}
|
||||
return derivedMaterial
|
||||
}
|
||||
set material(baseMaterial) {
|
||||
this._baseMaterial = baseMaterial;
|
||||
}
|
||||
|
||||
// Create and update material for shadows upon request:
|
||||
get customDepthMaterial() {
|
||||
return this.material.getDepthMaterial()
|
||||
}
|
||||
get customDistanceMaterial() {
|
||||
return this.material.getDistanceMaterial()
|
||||
}
|
||||
|
||||
onBeforeRender(shaderInfo) {
|
||||
const {uniforms} = this.material;
|
||||
const {pointA, controlA, controlB, pointB, radius, dashArray, dashOffset} = this;
|
||||
uniforms.pointA.value.copy(pointA);
|
||||
uniforms.controlA.value.copy(controlA);
|
||||
uniforms.controlB.value.copy(controlB);
|
||||
uniforms.pointB.value.copy(pointB);
|
||||
uniforms.radius.value = radius;
|
||||
uniforms.dashing.value.set(dashArray.x, dashArray.y, dashOffset || 0);
|
||||
}
|
||||
|
||||
raycast(raycaster, intersects) {
|
||||
// TODO - just fail for now
|
||||
}
|
||||
}
|
||||
|
||||
export { BezierMesh, createDerivedMaterial, expandShaderIncludes, getShaderUniformTypes, getShadersForMaterial, invertMatrix4, voidMainRegExp };
|
||||
Reference in New Issue
Block a user