troika-three-utils
Advanced tools
Comparing version 0.16.0 to 0.17.0
@@ -1,2 +0,2 @@ | ||
import ShaderFloatArray, {encodeFloatToFourInts, decodeFloatFromFourInts } from '../src/ShaderFloatArray' | ||
import { encodeFloatToFourInts, decodeFloatFromFourInts } from '../src/ShaderFloatArray.js' | ||
@@ -89,2 +89,2 @@ | ||
//TODO | ||
}) | ||
}) |
@@ -7,3 +7,3 @@ import { ShaderChunk, UniformsUtils, MeshDepthMaterial, RGBADepthPacking, MeshDistanceMaterial, ShaderLib, DataTexture, Vector3, MeshStandardMaterial, DoubleSide, Mesh, CylinderBufferGeometry, Vector2 } from 'three'; | ||
*/ | ||
var voidMainRegExp = /\bvoid\s+main\s*\(\s*\)\s*{/g; | ||
const voidMainRegExp = /\bvoid\s+main\s*\(\s*\)\s*{/g; | ||
@@ -18,5 +18,5 @@ /** | ||
function expandShaderIncludes( source ) { | ||
var pattern = /^[ \t]*#include +<([\w\d./]+)>/gm; | ||
const pattern = /^[ \t]*#include +<([\w\d./]+)>/gm; | ||
function replace(match, include) { | ||
var chunk = ShaderChunk[include]; | ||
let chunk = ShaderChunk[include]; | ||
return chunk ? expandShaderIncludes(chunk) : match | ||
@@ -28,10 +28,8 @@ } | ||
// Local assign polyfill to avoid importing troika-core | ||
var assign = Object.assign || function(/*target, ...sources*/) { | ||
var arguments$1 = arguments; | ||
var target = arguments[0]; | ||
for (var i = 1, len = arguments.length; i < len; i++) { | ||
var source = arguments$1[i]; | ||
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 (var prop in source) { | ||
for (let prop in source) { | ||
if (source.hasOwnProperty(prop)) { | ||
@@ -47,5 +45,5 @@ target[prop] = source[prop]; | ||
var idCtr = 0; | ||
var epoch = Date.now(); | ||
var CACHE = new WeakMap(); //threejs requires WeakMap internally so should be safe to assume support | ||
let idCtr = 0; | ||
const epoch = Date.now(); | ||
const CACHE = new WeakMap(); //threejs requires WeakMap internally so should be safe to assume support | ||
@@ -100,4 +98,4 @@ | ||
// which is faster and allows their shader program to be shared when rendering. | ||
var optionsHash = getOptionsHash(options); | ||
var cached = CACHE.get(baseMaterial); | ||
const optionsHash = getOptionsHash(options); | ||
let cached = CACHE.get(baseMaterial); | ||
if (!cached) { | ||
@@ -111,6 +109,6 @@ cached = Object.create(null); | ||
var id = ++idCtr; | ||
var privateDerivedShadersProp = "_derivedShaders" + id; | ||
var privateBeforeCompileProp = "_onBeforeCompile" + id; | ||
var distanceMaterialTpl, depthMaterialTpl; | ||
const id = ++idCtr; | ||
const privateDerivedShadersProp = `_derivedShaders${id}`; | ||
const privateBeforeCompileProp = `_onBeforeCompile${id}`; | ||
let distanceMaterialTpl, depthMaterialTpl; | ||
@@ -123,7 +121,5 @@ // Private onBeforeCompile handler that injects the modified shaders and uniforms when | ||
// Upgrade the shaders, caching the result | ||
var ref = this[privateDerivedShadersProp] || (this[privateDerivedShadersProp] = {vertex: {}, fragment: {}}); | ||
var vertex = ref.vertex; | ||
var fragment = ref.fragment; | ||
const {vertex, fragment} = this[privateDerivedShadersProp] || (this[privateDerivedShadersProp] = {vertex: {}, fragment: {}}); | ||
if (vertex.source !== shaderInfo.vertexShader || fragment.source !== shaderInfo.fragmentShader) { | ||
var upgraded = upgradeShaders(shaderInfo, options, id); | ||
const upgraded = upgradeShaders(shaderInfo, options, id); | ||
vertex.source = shaderInfo.vertexShader; | ||
@@ -163,6 +159,6 @@ vertex.result = upgraded.vertexShader; | ||
onBeforeCompile: { | ||
get: function get() { | ||
get() { | ||
return onBeforeCompile | ||
}, | ||
set: function set(fn) { | ||
set(fn) { | ||
this[privateBeforeCompileProp] = fn; | ||
@@ -189,3 +185,3 @@ } | ||
getDepthMaterial: {value: function() { | ||
var depthMaterial = this._depthMaterial; | ||
let depthMaterial = this._depthMaterial; | ||
if (!depthMaterial) { | ||
@@ -211,3 +207,3 @@ if (!depthMaterialTpl) { | ||
getDistanceMaterial: {value: function() { | ||
var distanceMaterial = this._distanceMaterial; | ||
let distanceMaterial = this._distanceMaterial; | ||
if (!distanceMaterial) { | ||
@@ -228,8 +224,6 @@ if (!distanceMaterialTpl) { | ||
dispose: {value: function value() { | ||
var ref = this; | ||
var _depthMaterial = ref._depthMaterial; | ||
var _distanceMaterial = ref._distanceMaterial; | ||
if (_depthMaterial) { _depthMaterial.dispose(); } | ||
if (_distanceMaterial) { _distanceMaterial.dispose(); } | ||
dispose: {value() { | ||
const {_depthMaterial, _distanceMaterial} = this; | ||
if (_depthMaterial) _depthMaterial.dispose(); | ||
if (_distanceMaterial) _distanceMaterial.dispose(); | ||
baseMaterial.dispose.call(this); | ||
@@ -239,3 +233,3 @@ }} | ||
var material = new DerivedMaterial(); | ||
const material = new DerivedMaterial(); | ||
material.copy(baseMaterial); | ||
@@ -254,17 +248,16 @@ | ||
function upgradeShaders(ref, options, id) { | ||
var vertexShader = ref.vertexShader; | ||
var fragmentShader = ref.fragmentShader; | ||
function upgradeShaders({vertexShader, fragmentShader}, options, id) { | ||
let { | ||
vertexDefs, | ||
vertexMainIntro, | ||
vertexTransform, | ||
fragmentDefs, | ||
fragmentMainIntro, | ||
fragmentColorTransform, | ||
timeUniform | ||
} = options; | ||
var vertexDefs = options.vertexDefs; | ||
var vertexMainIntro = options.vertexMainIntro; | ||
var vertexTransform = options.vertexTransform; | ||
var fragmentDefs = options.fragmentDefs; | ||
var fragmentMainIntro = options.fragmentMainIntro; | ||
var fragmentColorTransform = options.fragmentColorTransform; | ||
var timeUniform = options.timeUniform; | ||
// Inject auto-updating time uniform if requested | ||
if (timeUniform) { | ||
var code = "\nuniform float " + timeUniform + ";\n"; | ||
const code = `\nuniform float ${timeUniform};\n`; | ||
vertexDefs = (vertexDefs || '') + code; | ||
@@ -282,7 +275,17 @@ fragmentDefs = (fragmentDefs || '') + code; | ||
vertexShader = expandShaderIncludes(vertexShader); | ||
vertexDefs = (vertexDefs || '') + "\nvoid troikaVertexTransform" + id + "(inout vec3 position, inout vec3 normal, inout vec2 uv) {\n " + vertexTransform + "\n}\n"; | ||
vertexShader = vertexShader.replace(/\b(position|normal|uv)\b/g, function (match, match1, index, fullStr) { | ||
return /\battribute\s+vec[23]\s+$/.test(fullStr.substr(0, index)) ? match1 : ("troika_" + match1 + "_" + id) | ||
vertexDefs = `${vertexDefs || ''} | ||
void troikaVertexTransform${id}(inout vec3 position, inout vec3 normal, inout vec2 uv) { | ||
${vertexTransform} | ||
} | ||
`; | ||
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}_${id}` | ||
}); | ||
vertexMainIntro = "\nvec3 troika_position_" + id + " = vec3(position);\nvec3 troika_normal_" + id + " = vec3(normal);\nvec2 troika_uv_" + id + " = vec2(uv);\ntroikaVertexTransform" + id + "(troika_position_" + id + ", troika_normal_" + id + ", troika_uv_" + id + ");\n" + (vertexMainIntro || '') + "\n"; | ||
vertexMainIntro = ` | ||
vec3 troika_position_${id} = vec3(position); | ||
vec3 troika_normal_${id} = vec3(normal); | ||
vec2 troika_uv_${id} = vec2(uv); | ||
troikaVertexTransform${id}(troika_position_${id}, troika_normal_${id}, troika_uv_${id}); | ||
${vertexMainIntro || ''} | ||
`; | ||
} | ||
@@ -292,3 +295,3 @@ | ||
voidMainRegExp, | ||
((vertexDefs || '') + "\n\n$&\n\n" + (vertexMainIntro || ''))); | ||
`${vertexDefs || ''}\n\n$&\n\n${vertexMainIntro || ''}`); | ||
} | ||
@@ -299,9 +302,17 @@ | ||
fragmentShader = expandShaderIncludes(fragmentShader); | ||
fragmentShader = fragmentShader.replace(voidMainRegExp, ("\n" + (fragmentDefs || '') + "\nvoid troikaOrigMain" + id + "() {\n" + (fragmentMainIntro || '') + "\n")); | ||
fragmentShader += "\nvoid main() {\n troikaOrigMain" + id + "();\n " + (fragmentColorTransform || '') + "\n}"; | ||
fragmentShader = fragmentShader.replace(voidMainRegExp, ` | ||
${fragmentDefs || ''} | ||
void troikaOrigMain${id}() { | ||
${fragmentMainIntro || ''} | ||
`); | ||
fragmentShader += ` | ||
void main() { | ||
troikaOrigMain${id}(); | ||
${fragmentColorTransform || ''} | ||
}`; | ||
} | ||
return { | ||
vertexShader: vertexShader, | ||
fragmentShader: fragmentShader | ||
vertexShader, | ||
fragmentShader | ||
} | ||
@@ -320,3 +331,3 @@ } | ||
// TODO how can we keep this from getting stale? | ||
var MATERIAL_TYPES_TO_SHADERS = { | ||
const MATERIAL_TYPES_TO_SHADERS = { | ||
MeshDepthMaterial: 'depth', | ||
@@ -347,3 +358,3 @@ MeshDistanceMaterial: 'distanceRGBA', | ||
function getShadersForMaterial(material) { | ||
var builtinType = MATERIAL_TYPES_TO_SHADERS[material.type]; | ||
let builtinType = MATERIAL_TYPES_TO_SHADERS[material.type]; | ||
return builtinType ? ShaderLib[builtinType] : material //TODO fallback for unknown type? | ||
@@ -359,5 +370,5 @@ } | ||
function getShaderUniformTypes(shader) { | ||
var uniformRE = /\buniform\s+(int|float|vec[234])\s+([A-Za-z_][\w]*)/g; | ||
var uniforms = Object.create(null); | ||
var match; | ||
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) { | ||
@@ -388,194 +399,202 @@ uniforms[match[2]] = match[1]; | ||
* TODO: | ||
* - Fix texture to fill both dimensions so we don't easily hit max texture size limits | ||
* - Use a float texture if the extension is available so we can skip the encoding process | ||
*/ | ||
var ShaderFloatArray = function ShaderFloatArray(name) { | ||
this.name = name; | ||
this.textureUniform = "dataTex_" + name; | ||
this.textureSizeUniform = "dataTexSize_" + name; | ||
this.multiplierUniform = "dataMultiplier_" + name; | ||
class ShaderFloatArray { | ||
constructor(name) { | ||
this.name = name; | ||
this.textureUniform = `dataTex_${name}`; | ||
this.textureSizeUniform = `dataTexSize_${name}`; | ||
this.multiplierUniform = `dataMultiplier_${name}`; | ||
/** | ||
* @property dataSizeUniform - the name of the GLSL uniform that will hold the | ||
* length of the data array. | ||
* @type {string} | ||
*/ | ||
this.dataSizeUniform = `dataSize_${name}`; | ||
/** | ||
* @property readFunction - the name of the GLSL function that should be called to | ||
* read data out of the array by index. | ||
* @type {string} | ||
*/ | ||
this.readFunction = `readData_${name}`; | ||
this._raw = new Float32Array(0); | ||
this._texture = new DataTexture(new Uint8Array(0), 0, 1); | ||
this._length = 0; | ||
this._multiplier = 1; | ||
} | ||
/** | ||
* @property dataSizeUniform - the name of the GLSL uniform that will hold the | ||
* length of the data array. | ||
* @type {string} | ||
* @property length - the current length of the data array | ||
* @type {number} | ||
*/ | ||
this.dataSizeUniform = "dataSize_" + name; | ||
set length(value) { | ||
if (value !== this._length) { | ||
// Find nearest power-of-2 that holds the new length | ||
const size = Math.pow(2, Math.ceil(Math.log2(value))); | ||
const raw = this._raw; | ||
if (size < raw.length) { | ||
this._raw = raw.subarray(0, size); | ||
} | ||
else if(size > raw.length) { | ||
this._raw = new Float32Array(size); | ||
this._raw.set(raw); | ||
} | ||
this._length = value; | ||
} | ||
} | ||
get length() { | ||
return this._length | ||
} | ||
/** | ||
* @property readFunction - the name of the GLSL function that should be called to | ||
* read data out of the array by index. | ||
* @type {string} | ||
* Add a value to the end of the data array | ||
* @param {number} value | ||
*/ | ||
this.readFunction = "readData_" + name; | ||
push(value) { | ||
return this.set(this.length++, value) | ||
} | ||
this._raw = new Float32Array(0); | ||
this._texture = new DataTexture(new Uint8Array(0), 0, 1); | ||
this._length = 0; | ||
this._multiplier = 1; | ||
}; | ||
/** | ||
* Replace the existing data with that from a new array | ||
* @param {ArrayLike<number>} array | ||
*/ | ||
setArray(array) { | ||
this.length = array.length; | ||
this._raw.set(array); | ||
this._needsRepack = true; | ||
} | ||
var prototypeAccessors = { length: { configurable: true } }; | ||
/** | ||
* Get the current value at index | ||
* @param {number} index | ||
* @return {number} | ||
*/ | ||
get(index) { | ||
return this._raw[index] | ||
} | ||
/** | ||
* @property length - the current length of the data array | ||
* @type {number} | ||
*/ | ||
prototypeAccessors.length.set = function (value) { | ||
if (value !== this._length) { | ||
// Find nearest power-of-2 that holds the new length | ||
var size = Math.pow(2, Math.ceil(Math.log2(value))); | ||
var raw = this._raw; | ||
if (size < raw.length) { | ||
this._raw = raw.subarray(0, size); | ||
set(index, value) { | ||
if (index + 1 > this._length) { | ||
this.length = index + 1; | ||
} | ||
else if(size > raw.length) { | ||
this._raw = new Float32Array(size); | ||
this._raw.set(raw); | ||
if (value !== this._raw[index]) { | ||
this._raw[index] = value; | ||
encodeFloatToFourInts( | ||
value / this._multiplier, | ||
this._texture.image.data, | ||
index * 4 | ||
); | ||
this._needsMultCheck = true; | ||
} | ||
this._length = value; | ||
} | ||
}; | ||
prototypeAccessors.length.get = function () { | ||
return this._length | ||
}; | ||
/** | ||
* Add a value to the end of the data array | ||
* @param {number} value | ||
*/ | ||
ShaderFloatArray.prototype.push = function push (value) { | ||
return this.set(this.length++, value) | ||
}; | ||
/** | ||
* Make a copy of this ShaderFloatArray | ||
* @return {ShaderFloatArray} | ||
*/ | ||
clone() { | ||
const clone = new ShaderFloatArray(this.name); | ||
clone.setArray(this._raw); | ||
return clone | ||
} | ||
/** | ||
* Replace the existing data with that from a new array | ||
* @param {ArrayLike<number>} array | ||
*/ | ||
ShaderFloatArray.prototype.setArray = function setArray (array) { | ||
this.length = array.length; | ||
this._raw.set(array); | ||
this._needsRepack = true; | ||
}; | ||
/** | ||
* Retrieve the set of Uniforms that must to be added to the target ShaderMaterial or | ||
* DerivedMaterial, to feed the GLSL code generated by {@link #getShaderHeaderCode}. | ||
* @return {Object} | ||
*/ | ||
getShaderUniforms() { | ||
const me = this; | ||
return { | ||
[this.textureUniform]: {get value() { | ||
me._sync(); | ||
return me._texture | ||
}}, | ||
[this.textureSizeUniform]: {get value() { | ||
me._sync(); | ||
return me._texture.image.width | ||
}}, | ||
[this.dataSizeUniform]: {get value() { | ||
me._sync(); | ||
return me.length | ||
}}, | ||
[this.multiplierUniform]: {get value() { | ||
me._sync(); | ||
return me._multiplier | ||
}} | ||
} | ||
} | ||
/** | ||
* Get the current value at index | ||
* @param {number} index | ||
* @return {number} | ||
*/ | ||
ShaderFloatArray.prototype.get = function get (index) { | ||
return this._raw[index] | ||
}; | ||
/** | ||
* Retrieve the GLSL code that must be injected into the shader's definitions area to | ||
* enable reading from the data array. This exposes a function with a name matching | ||
* the {@link #readFunction} property, which other shader code can call to read values | ||
* from the array by their index. | ||
* @return {string} | ||
*/ | ||
getShaderHeaderCode() { | ||
const {textureUniform, textureSizeUniform, dataSizeUniform, multiplierUniform, readFunction} = this; | ||
return ` | ||
uniform sampler2D ${textureUniform}; | ||
uniform float ${textureSizeUniform}; | ||
uniform float ${dataSizeUniform}; | ||
uniform float ${multiplierUniform}; | ||
ShaderFloatArray.prototype.set = function set (index, value) { | ||
if (index + 1 > this._length) { | ||
this.length = index + 1; | ||
float ${readFunction}(float index) { | ||
vec2 texUV = vec2((index + 0.5) / ${textureSizeUniform}, 0.5); | ||
vec4 pixel = texture2D(${textureUniform}, texUV); | ||
return dot(pixel, 1.0 / vec4(1.0, 255.0, 65025.0, 16581375.0)) * ${multiplierUniform}; | ||
} | ||
` | ||
} | ||
if (value !== this._raw[index]) { | ||
this._raw[index] = value; | ||
encodeFloatToFourInts( | ||
value / this._multiplier, | ||
this._texture.image.data, | ||
index * 4 | ||
); | ||
this._needsMultCheck = true; | ||
} | ||
}; | ||
/** | ||
* Make a copy of this ShaderFloatArray | ||
* @return {ShaderFloatArray} | ||
*/ | ||
ShaderFloatArray.prototype.clone = function clone () { | ||
var clone = new ShaderFloatArray(this.name); | ||
clone.setArray(this._raw); | ||
return clone | ||
}; | ||
/** | ||
* @private Synchronize any pending changes to the underlying DataTexture | ||
*/ | ||
_sync() { | ||
const tex = this._texture; | ||
const raw = this._raw; | ||
let needsRepack = this._needsRepack; | ||
/** | ||
* Retrieve the set of Uniforms that must to be added to the target ShaderMaterial or | ||
* DerivedMaterial, to feed the GLSL code generated by {@link #getShaderHeaderCode}. | ||
* @return {Object} | ||
*/ | ||
ShaderFloatArray.prototype.getShaderUniforms = function getShaderUniforms () { | ||
var obj; | ||
var me = this; | ||
return ( obj = {}, obj[this.textureUniform] = {get value() { | ||
me._sync(); | ||
return me._texture | ||
}}, obj[this.textureSizeUniform] = {get value() { | ||
me._sync(); | ||
return me._texture.image.width | ||
}}, obj[this.dataSizeUniform] = {get value() { | ||
me._sync(); | ||
return me.length | ||
}}, obj[this.multiplierUniform] = {get value() { | ||
me._sync(); | ||
return me._multiplier | ||
}}, obj ) | ||
}; | ||
/** | ||
* Retrieve the GLSL code that must be injected into the shader's definitions area to | ||
* enable reading from the data array. This exposes a function with a name matching | ||
* the {@link #readFunction} property, which other shader code can call to read values | ||
* from the array by their index. | ||
* @return {string} | ||
*/ | ||
ShaderFloatArray.prototype.getShaderHeaderCode = function getShaderHeaderCode () { | ||
var ref = this; | ||
var textureUniform = ref.textureUniform; | ||
var textureSizeUniform = ref.textureSizeUniform; | ||
var dataSizeUniform = ref.dataSizeUniform; | ||
var multiplierUniform = ref.multiplierUniform; | ||
var readFunction = ref.readFunction; | ||
return ("\nuniform sampler2D " + textureUniform + ";\nuniform float " + textureSizeUniform + ";\nuniform float " + dataSizeUniform + ";\nuniform float " + multiplierUniform + ";\n\nfloat " + readFunction + "(float index) {\n vec2 texUV = vec2((index + 0.5) / " + textureSizeUniform + ", 0.5);\n vec4 pixel = texture2D(" + textureUniform + ", texUV);\n return dot(pixel, 1.0 / vec4(1.0, 255.0, 65025.0, 16581375.0)) * " + multiplierUniform + ";\n}\n") | ||
}; | ||
/** | ||
* @private Synchronize any pending changes to the underlying DataTexture | ||
*/ | ||
ShaderFloatArray.prototype._sync = function _sync () { | ||
var tex = this._texture; | ||
var raw = this._raw; | ||
var needsRepack = this._needsRepack; | ||
// If the size of the raw array changed, resize the texture to match | ||
if (raw.length !== tex.image.width) { | ||
tex.image = { | ||
data: new Uint8Array(raw.length * 4), | ||
width: raw.length, | ||
height: 1 | ||
}; | ||
needsRepack = true; | ||
} | ||
// If the values changed, check the multiplier. This should be a value by which | ||
// all the values are divided to constrain them to the [0,1] range required by | ||
// the Uint8 packing algorithm. We pick the nearest power of 2 that holds the | ||
// maximum value for greatest accuracy. | ||
if (needsRepack || this._needsMultCheck) { | ||
var maxVal = this._raw.reduce(function (a, b) { return Math.max(a, b); }, 0); | ||
var mult = Math.pow(2, Math.ceil(Math.log2(maxVal))); | ||
if (mult !== this._multiplier) { | ||
this._multiplier = mult; | ||
// If the size of the raw array changed, resize the texture to match | ||
if (raw.length !== tex.image.width) { | ||
tex.image = { | ||
data: new Uint8Array(raw.length * 4), | ||
width: raw.length, | ||
height: 1 | ||
}; | ||
needsRepack = true; | ||
} | ||
tex.needsUpdate = true; | ||
this._needsMultCheck = false; | ||
} | ||
// If things changed in a way we need to repack, do so | ||
if (needsRepack) { | ||
for (var i = 0, len = raw.length, mult$1 = this._multiplier; i < len; i++) { | ||
encodeFloatToFourInts(raw[i] / mult$1, tex.image.data, i * 4); | ||
// If the values changed, check the multiplier. This should be a value by which | ||
// all the values are divided to constrain them to the [0,1] range required by | ||
// the Uint8 packing algorithm. We pick the nearest power of 2 that holds the | ||
// maximum value for greatest accuracy. | ||
if (needsRepack || this._needsMultCheck) { | ||
const maxVal = this._raw.reduce((a, b) => Math.max(a, b), 0); | ||
const mult = Math.pow(2, Math.ceil(Math.log2(maxVal))); | ||
if (mult !== this._multiplier) { | ||
this._multiplier = mult; | ||
needsRepack = true; | ||
} | ||
tex.needsUpdate = true; | ||
this._needsMultCheck = false; | ||
} | ||
this._needsRepack = false; | ||
// If things changed in a way we need to repack, do so | ||
if (needsRepack) { | ||
for (let i = 0, len = raw.length, mult = this._multiplier; i < len; i++) { | ||
encodeFloatToFourInts(raw[i] / mult, tex.image.data, i * 4); | ||
} | ||
this._needsRepack = false; | ||
} | ||
} | ||
}; | ||
} | ||
Object.defineProperties( ShaderFloatArray.prototype, prototypeAccessors ); | ||
/** | ||
@@ -605,6 +624,6 @@ * Encode a floating point number into a set of four 8-bit integers. | ||
// the range [0, 1]. The maximum error after encoding and decoding is ~1.18e-10 | ||
var enc0 = 255 * value; | ||
var enc1 = 255 * (enc0 % 1); | ||
var enc2 = 255 * (enc1 % 1); | ||
var enc3 = 255 * (enc2 % 1); | ||
let enc0 = 255 * value; | ||
let enc1 = 255 * (enc0 % 1); | ||
let enc2 = 255 * (enc1 % 1); | ||
let enc3 = 255 * (enc2 % 1); | ||
@@ -628,10 +647,70 @@ enc0 = enc0 & 255; | ||
var vertexDefs = "\nuniform vec3 pointA;\nuniform vec3 controlA;\nuniform vec3 controlB;\nuniform vec3 pointB;\nuniform float radius;\nvarying float bezierT;\n\nvec3 cubicBezier(vec3 p1, vec3 c1, vec3 c2, vec3 p2, float t) {\n float t2 = 1.0 - t;\n float b0 = t2 * t2 * t2;\n float b1 = 3.0 * t * t2 * t2;\n float b2 = 3.0 * t * t * t2;\n float b3 = t * t * t;\n return b0 * p1 + b1 * c1 + b2 * c2 + b3 * p2;\n}\n\nvec3 cubicBezierDerivative(vec3 p1, vec3 c1, vec3 c2, vec3 p2, float t) {\n float t2 = 1.0 - t;\n return -3.0 * p1 * t2 * t2 +\n c1 * (3.0 * t2 * t2 - 6.0 * t2 * t) +\n c2 * (6.0 * t2 * t - 3.0 * t * t) +\n 3.0 * p2 * t * t;\n}\n"; | ||
const vertexDefs = ` | ||
uniform vec3 pointA; | ||
uniform vec3 controlA; | ||
uniform vec3 controlB; | ||
uniform vec3 pointB; | ||
uniform float radius; | ||
varying float bezierT; | ||
var vertexTransform = "\nfloat t = position.y;\nbezierT = t;\nvec3 bezierCenterPos = cubicBezier(pointA, controlA, controlB, pointB, t);\nvec3 bezierDir = normalize(cubicBezierDerivative(pointA, controlA, controlB, pointB, t));\n\n// Make \"sideways\" always perpendicular to the camera ray; this ensures that any twists\n// in the cylinder occur where you won't see them: \nvec3 viewDirection = normalMatrix * vec3(0.0, 0.0, 1.0);\nif (bezierDir == viewDirection) {\n bezierDir = normalize(cubicBezierDerivative(pointA, controlA, controlB, pointB, t == 1.0 ? t - 0.0001 : t + 0.0001));\n}\nvec3 sideways = normalize(cross(bezierDir, viewDirection));\nvec3 upish = normalize(cross(sideways, bezierDir));\n\n// Build a matrix for transforming this disc in the cylinder:\nmat4 discTx;\ndiscTx[0].xyz = sideways * radius;\ndiscTx[1].xyz = bezierDir * radius;\ndiscTx[2].xyz = upish * radius;\ndiscTx[3].xyz = bezierCenterPos;\ndiscTx[3][3] = 1.0;\n\n// Apply transform, ignoring original y\nposition = (discTx * vec4(position.x, 0.0, position.z, 1.0)).xyz;\nnormal = normalize(mat3(discTx) * normal);\n"; | ||
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; | ||
} | ||
var fragmentDefs = "\nuniform vec3 dashing;\nvarying float bezierT;\n"; | ||
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; | ||
} | ||
`; | ||
var fragmentMainIntro = "\nif (dashing.x + dashing.y > 0.0) {\n float dashFrac = mod(bezierT - dashing.z, dashing.x + dashing.y);\n if (dashFrac > dashing.x) {\n discard;\n }\n}\n"; | ||
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: | ||
@@ -663,6 +742,6 @@ // const fragmentColorTransform = ` | ||
}, | ||
vertexDefs: vertexDefs, | ||
vertexTransform: vertexTransform, | ||
fragmentDefs: fragmentDefs, | ||
fragmentMainIntro: fragmentMainIntro | ||
vertexDefs, | ||
vertexTransform, | ||
fragmentDefs, | ||
fragmentMainIntro | ||
} | ||
@@ -672,5 +751,5 @@ ) | ||
var geometry = null; | ||
let geometry = null; | ||
var defaultBaseMaterial = new MeshStandardMaterial({color: 0xffffff, side: DoubleSide}); | ||
const defaultBaseMaterial = new MeshStandardMaterial({color: 0xffffff, side: DoubleSide}); | ||
@@ -705,6 +784,6 @@ | ||
*/ | ||
var BezierMesh = /*@__PURE__*/(function (Mesh) { | ||
function BezierMesh() { | ||
Mesh.call( | ||
this, geometry || (geometry = | ||
class BezierMesh extends Mesh { | ||
constructor() { | ||
super( | ||
geometry || (geometry = | ||
new CylinderBufferGeometry(1, 1, 1, 6, 64).translate(0, 0.5, 0) | ||
@@ -728,13 +807,7 @@ ), | ||
if ( Mesh ) BezierMesh.__proto__ = Mesh; | ||
BezierMesh.prototype = Object.create( Mesh && Mesh.prototype ); | ||
BezierMesh.prototype.constructor = BezierMesh; | ||
var prototypeAccessors = { material: { configurable: true },customDepthMaterial: { configurable: true },customDistanceMaterial: { configurable: true } }; | ||
// 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. | ||
prototypeAccessors.material.get = function () { | ||
var derivedMaterial = this._derivedMaterial; | ||
var baseMaterial = this._baseMaterial || defaultBaseMaterial; | ||
get material() { | ||
let derivedMaterial = this._derivedMaterial; | ||
const baseMaterial = this._baseMaterial || defaultBaseMaterial; | ||
if (!derivedMaterial || derivedMaterial.baseMaterial !== baseMaterial) { | ||
@@ -752,29 +825,22 @@ if (derivedMaterial) { | ||
return derivedMaterial | ||
}; | ||
prototypeAccessors.material.set = function (baseMaterial) { | ||
} | ||
set material(baseMaterial) { | ||
this._baseMaterial = baseMaterial; | ||
}; | ||
} | ||
// Create and update material for shadows upon request: | ||
prototypeAccessors.customDepthMaterial.get = function () { | ||
get customDepthMaterial() { | ||
return this._updateBezierUniforms(this.material.getDepthMaterial()) | ||
}; | ||
prototypeAccessors.customDistanceMaterial.get = function () { | ||
} | ||
get customDistanceMaterial() { | ||
return this._updateBezierUniforms(this.material.getDistanceMaterial()) | ||
}; | ||
} | ||
BezierMesh.prototype.onBeforeRender = function onBeforeRender (shaderInfo) { | ||
onBeforeRender(shaderInfo) { | ||
this._updateBezierUniforms(this.material); | ||
}; | ||
} | ||
BezierMesh.prototype._updateBezierUniforms = function _updateBezierUniforms (material) { | ||
var uniforms = material.uniforms; | ||
var ref = this; | ||
var pointA = ref.pointA; | ||
var controlA = ref.controlA; | ||
var controlB = ref.controlB; | ||
var pointB = ref.pointB; | ||
var radius = ref.radius; | ||
var dashArray = ref.dashArray; | ||
var dashOffset = ref.dashOffset; | ||
_updateBezierUniforms(material) { | ||
const {uniforms} = material; | ||
const {pointA, controlA, controlB, pointB, radius, dashArray, dashOffset} = this; | ||
uniforms.pointA.value.copy(pointA); | ||
@@ -787,13 +853,9 @@ uniforms.controlA.value.copy(controlA); | ||
return material | ||
}; | ||
} | ||
BezierMesh.prototype.raycast = function raycast (raycaster, intersects) { | ||
raycast(raycaster, intersects) { | ||
// TODO - just fail for now | ||
}; | ||
} | ||
} | ||
Object.defineProperties( BezierMesh.prototype, prototypeAccessors ); | ||
return BezierMesh; | ||
}(Mesh)); | ||
export { BezierMesh, ShaderFloatArray, createDerivedMaterial, expandShaderIncludes, getShaderUniformTypes, getShadersForMaterial, voidMainRegExp }; |
@@ -373,2 +373,3 @@ (function (global, factory) { | ||
* TODO: | ||
* - Fix texture to fill both dimensions so we don't easily hit max texture size limits | ||
* - Use a float texture if the extension is available so we can skip the encoding process | ||
@@ -375,0 +376,0 @@ */ |
{ | ||
"name": "troika-three-utils", | ||
"version": "0.16.0", | ||
"version": "0.17.0", | ||
"description": "Various utilities related to Three.js", | ||
@@ -17,3 +17,3 @@ "author": "Jason Johnston <jason.johnston@protectwise.com>", | ||
"module:es2015": "src/index.js", | ||
"gitHead": "b17e3967b2166f102cabc6cd4bf46f5a725a8910" | ||
"gitHead": "31909f4beed23f5913aa380db1dd3c1802798b39" | ||
} |
@@ -1,4 +0,82 @@ | ||
# `troika-three-utils` | ||
# Troika Three.js Utilities | ||
> Various utilities for working with [Three.js](https://github.com/mrdoob/three.js) | ||
This package provides various utilities for working with [Three.js](https://github.com/mrdoob/three.js), particularly having to do with shaders. It is used by [Troika 3D](../troika-3d), but has no dependencies itself other than Three.js, so it can be used outside the Troika framework. | ||
## Installation | ||
Get it from [NPM](https://www.npmjs.com/package/troika-three-utils): | ||
```sh | ||
npm install troika-three-utils | ||
``` | ||
You will also need to install a compatible version of [Three.js](https://threejs.org); see the notes in the [Troika 3D Readme](../troika-3d/README.md#installation) for details. | ||
## Provided Utilities | ||
Several utilities are provided; for a full list follow the imports in [index.js](./src/index.js) to their source files, where each is documented in JSDoc comments. | ||
Some of the most useful ones are: | ||
### createDerivedMaterial() | ||
_[Source code with JSDoc](./src/DerivedMaterial.js)_ | ||
One of the most powerful things about Three.js is its excellent set of built-in materials. They provide many features like physically-based reflectivity, shadows, texture maps, fog, and so on, building the very complex shaders behind the scenes. | ||
But sometimes you need to do something custom in the shaders, such as move around the vertices, or change the colors or transparency of certain pixels. You could use a [ShaderMaterial](https://threejs.org/docs/#api/en/materials/ShaderMaterial) but then you lose all the built-in features. The experimental [NodeMaterial](https://www.donmccurdy.com/2019/03/17/three-nodematerial-introduction/) seems promising but doesn't appear to be ready as a full replacement. | ||
The [onBeforeCompile](https://threejs.org/docs/#api/en/materials/Material.onBeforeCompile) hook lets you intercept the shader code and modify it, but in practice there are quirks to this that make it difficult to work with, not to mention the complexity of forming regular expressions to inject your custom shader code in the right places. | ||
Troika's `createDerivedMaterial(baseMaterial, options)` utility handles all that complexity, letting you "extend" a built-in Material's shaders via a declarative interface. The resulting material is prototype-chained to the base material so it picks up changes to its properties. It has methods for generating depth and distance materials so your shader modifications can be reflected in shadow maps. Derived materials may themselves be derived from recursively for composability. | ||
Here's a simple example that injects an auto-incrementing `elapsed` uniform holding the current time, and uses that to transform the vertices in a wave pattern. | ||
```js | ||
import { createDerivedMaterial} from 'troika-three-utils' | ||
import { Mesh, MeshStandardMaterial, PlaneBufferGeometry } from 'three' | ||
const baseMaterial = new MeshStandardMaterial({color: 0xffcc00}) | ||
const customMaterial = createDerivedMaterial( | ||
baseMaterial, | ||
{ | ||
timeUniform: 'elapsed', | ||
vertexTransform: ` | ||
float waveAmplitude = 0.1 | ||
float waveX = uv.x * PI * 4.0 - mod(elapsed / 300.0, PI2); | ||
float waveZ = sin(waveX) * waveAmplitude; | ||
normal.xyz = normalize(vec3(-cos(waveX) * waveAmplitude, 0.0, 1.0)); | ||
position.z += waveZ; | ||
` | ||
} | ||
) | ||
const mesh = new Mesh( | ||
new PlaneBufferGeometry(1, 1, 64, 1), | ||
customMaterial | ||
) | ||
mesh.customDepthMaterial = customMaterial.getDepthMaterial() //for shadows | ||
``` | ||
You can also declare custom `uniforms` and `defines`, inject fragment shader code to modify the output color, etc. See the JSDoc in [DerivedMaterial.js](./src/DerivedMaterial.js) for full details. | ||
### BezierMesh | ||
_[Source code with JSDoc](./src/BezierMesh.js)_ | _[Online example](https://troika-examples.netlify.com/#bezier3d)_ | ||
This creates a cylindrical mesh and bends it along a 3D cubic bezier path between two points, in a custom derived vertex shader. This is useful for visually connecting objects in 3D space with a line that has thickness to it. | ||
```js | ||
import { BezierMesh } from 'troika-three-utils' | ||
const bezier = new BezierMesh() | ||
bezier.pointA.set(-0.3, 0.4, -0.3) | ||
bezier.controlA.set(0.7, 0.6, 0.4) | ||
bezier.controlB.set(-0.6, -0.6, -0.6) | ||
bezier.pointB.set(0.7, 0, -0.7) | ||
bezier.radius = 0.01 | ||
scene.add(bezier) | ||
``` | ||
import { CylinderBufferGeometry, DoubleSide, Mesh, MeshStandardMaterial, Vector2, Vector3 } from 'three' | ||
import { createBezierMeshMaterial } from './BezierMeshMaterial' | ||
import { createBezierMeshMaterial } from './BezierMeshMaterial.js' | ||
@@ -4,0 +4,0 @@ let geometry = null |
@@ -1,2 +0,2 @@ | ||
import { createDerivedMaterial } from './DerivedMaterial' | ||
import { createDerivedMaterial } from './DerivedMaterial.js' | ||
import { Vector2, Vector3 } from 'three' | ||
@@ -109,2 +109,2 @@ | ||
) | ||
} | ||
} |
@@ -10,2 +10,2 @@ // Troika Three.js Utilities exports | ||
export {voidMainRegExp} from './voidMainRegExp.js' | ||
export {BezierMesh} from './BezierMesh' | ||
export {BezierMesh} from './BezierMesh.js' |
@@ -24,2 +24,3 @@ import { | ||
* TODO: | ||
* - Fix texture to fill both dimensions so we don't easily hit max texture size limits | ||
* - Use a float texture if the extension is available so we can skip the encoding process | ||
@@ -26,0 +27,0 @@ */ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
111393
2352
83
2