Favivon - Correção

This commit is contained in:
2026-05-30 19:59:39 -03:00
parent 76ddaa815d
commit d7dfd221f0
32859 changed files with 5459654 additions and 404 deletions
+422
View File
@@ -0,0 +1,422 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.52.4](https://github.com/protectwise/troika/compare/v0.52.3...v0.52.4) (2025-04-02)
### Bug Fixes
* error in Three r175 with custom(Depth|Distance)Material not having setters ([78e00b5](https://github.com/protectwise/troika/commit/78e00b512eb9618623d248070ba75ea0c55803ee)), closes [#357](https://github.com/protectwise/troika/issues/357)
# [0.52.0](https://github.com/protectwise/troika/compare/v0.51.1...v0.52.0) (2024-11-11)
**Note:** Version bump only for package troika-three-utils
# [0.51.0](https://github.com/protectwise/troika/compare/v0.50.3...v0.51.0) (2024-11-10)
**Note:** Version bump only for package troika-three-utils
## [0.50.3](https://github.com/protectwise/troika/compare/v0.50.2...v0.50.3) (2024-11-01)
### Bug Fixes
* Error in derived materials in Three r170 ([2402026](https://github.com/protectwise/troika/commit/240202662447040c0255f39e6c296207ba9a5457))
# [0.50.0](https://github.com/protectwise/troika/compare/v0.49.1...v0.50.0) (2024-10-11)
### Bug Fixes
* **troika-three-utils:** add `colorspace_fragment` (introduced in Three r154) to the list of fragment shaders to process in `upgradeShaders()` ([#325](https://github.com/protectwise/troika/issues/325)) ([9169ebf](https://github.com/protectwise/troika/commit/9169ebf0773f9ad6cd10ab74456b68d1eebbe243))
* change JSDoc for createDerivedMaterial params to be optional ([#307](https://github.com/protectwise/troika/issues/307)) ([6969e85](https://github.com/protectwise/troika/commit/6969e855608803d20c6634a415d2a78218222d79))
### Features
* **troika-three-utils:** add isDerivedFrom method to derived materials ([a4a2fcd](https://github.com/protectwise/troika/commit/a4a2fcd18ddf03cf6f9b6b72df2e2fbdfa700c51))
# [0.49.0](https://github.com/protectwise/troika/compare/v0.48.1...v0.49.0) (2023-10-08)
**Note:** Version bump only for package troika-three-utils
# [0.48.0](https://github.com/protectwise/troika/compare/v0.47.2...v0.48.0) (2023-09-09)
**Note:** Version bump only for package troika-three-utils
## [0.47.2](https://github.com/protectwise/troika/compare/v0.47.1...v0.47.2) (2023-05-15)
### Bug Fixes
* **troika-three-utils:** pass renderer argument to base material's onBeforeCompile ([3a23a8e](https://github.com/protectwise/troika/commit/3a23a8eb72275e37d42085cb6a2fd4e704224e9d))
* **troika-three-utils:** vertexTransform now handles MAP_UV token introduced in Three r152 ([d8c32ed](https://github.com/protectwise/troika/commit/d8c32ed93920633455e078283b0c3dbc058fb7c1))
# [0.47.0](https://github.com/protectwise/troika/compare/v0.46.3...v0.47.0) (2022-12-15)
### Bug Fixes
* Remove incomplete ShaderFloatArray utility to fix build ([ddf712a](https://github.com/protectwise/troika/commit/ddf712af8e9ae164527dfe9cabcb2698eb809dc5))
### Features
* raise three min version to r125 and fix BufferGeometry references ([#225](https://github.com/protectwise/troika/issues/225)) ([f2ef803](https://github.com/protectwise/troika/commit/f2ef803db7ab3d9d03de2719a2781c1c3f5122cf))
# [0.46.0](https://github.com/protectwise/troika/compare/v0.45.0...v0.46.0) (2022-03-05)
**Note:** Version bump only for package troika-three-utils
# [0.45.0](https://github.com/protectwise/troika/compare/v0.44.0...v0.45.0) (2022-01-02)
### Bug Fixes
* **createDerivedMaterial:** propagate the base material's customProgramCacheKey ([9c8c6b1](https://github.com/protectwise/troika/commit/9c8c6b1e472f4068a8c6b0f1a6f3e32c935f8077))
# [0.44.0](https://github.com/protectwise/troika/compare/v0.43.1-alpha.0...v0.44.0) (2021-11-14)
**Note:** Version bump only for package troika-three-utils
# [0.43.0](https://github.com/protectwise/troika/compare/v0.42.0...v0.43.0) (2021-09-20)
### Bug Fixes
* **InstancedUniformsMesh:** reuse same derived material instance when uniform names change ([bd7cea6](https://github.com/protectwise/troika/commit/bd7cea6116556f9fb4ba3e6710ed07f871739e93)), closes [#154](https://github.com/protectwise/troika/issues/154)
# [0.42.0](https://github.com/protectwise/troika/compare/v0.41.2...v0.42.0) (2021-05-17)
### Features
* open up 'three' peer dependency to include future versions ([d4a5b23](https://github.com/protectwise/troika/commit/d4a5b2376fffb3681750761f757b684ab798315a))
# [0.41.0](https://github.com/protectwise/troika/compare/v0.40.0...v0.41.0) (2021-04-19)
### Features
* update support up to threejs r127 ([5b512f0](https://github.com/protectwise/troika/commit/5b512f04843f15bdaadd7297d6ab1d964c67333b))
# [0.40.0](https://github.com/protectwise/troika/compare/v0.39.2...v0.40.0) (2021-02-28)
**Note:** Version bump only for package troika-three-utils
# [0.39.0](https://github.com/protectwise/troika/compare/v0.38.1...v0.39.0) (2021-02-15)
### Bug Fixes
* restore compatibility with three versions <0.113.0 by copying MathUtils.generateUUID ([35856b5](https://github.com/protectwise/troika/commit/35856b555919278b1addad0d2625faaaeb379757))
## [0.38.1](https://github.com/protectwise/troika/compare/v0.38.0...v0.38.1) (2021-02-03)
### Bug Fixes
* hoist vertexTransform defs to top of the shader to prevent errors in chained derivations ([889ed38](https://github.com/protectwise/troika/commit/889ed38fcfb30edc630865b6e95f59c3f6322646))
* make derived material's customProgramCacheKey function writable ([10289dd](https://github.com/protectwise/troika/commit/10289dd1fc700facff9ab79fb6e1cc04109fc0ff))
* update to support up to Three r125 ([4edff04](https://github.com/protectwise/troika/commit/4edff042d13dec49377d18baf4f958de285a3f2a))
# [0.38.0](https://github.com/protectwise/troika/compare/v0.37.0...v0.38.0) (2021-01-24)
### Features
* move InstancedUniformsMesh to its own three-instanced-uniforms-mesh package ([f623b1f](https://github.com/protectwise/troika/commit/f623b1f2307b0db094912246ee4cf4bef54ffd85))
# [0.37.0](https://github.com/protectwise/troika/compare/v0.36.1...v0.37.0) (2021-01-18)
### Features
* add InstancedUniformsMesh class for setting shader uniforms per instance ([5fd4d79](https://github.com/protectwise/troika/commit/5fd4d797740096dc66a4da73f49961158e5bda2f))
# [0.36.0](https://github.com/protectwise/troika/compare/v0.35.0...v0.36.0) (2020-12-04)
**Note:** Version bump only for package troika-three-utils
# [0.35.0](https://github.com/protectwise/troika/compare/v0.34.2...v0.35.0) (2020-11-16)
**Note:** Version bump only for package troika-three-utils
# [0.34.0](https://github.com/protectwise/troika/compare/v0.33.1...v0.34.0) (2020-10-19)
**Note:** Version bump only for package troika-three-utils
## [0.33.1](https://github.com/protectwise/troika/compare/v0.33.0...v0.33.1) (2020-10-02)
**Note:** Version bump only for package troika-three-utils
# [0.33.0](https://github.com/protectwise/troika/compare/v0.32.0...v0.33.0) (2020-10-02)
### Bug Fixes
* add "sideEffects":false to package.json files to assist treeshaking ([61109b2](https://github.com/protectwise/troika/commit/61109b2e3d21dc794ef66b3f28cf63bbdd34150e))
* add PURE annotations to make troika-three-text treeshakeable ([8e76b5c](https://github.com/protectwise/troika/commit/8e76b5c31a3cbda86595654ba9d66d8d635e44a1))
* remove redundant "browser" and defunct "jsnext:main" fields from package.json files ([0abec40](https://github.com/protectwise/troika/commit/0abec40e3af06d3ae4d990bf198d871b46730f1f))
### Features
* **troika-three-utils:** add `chained` option to createDerivedMaterial ([2bfaa9c](https://github.com/protectwise/troika/commit/2bfaa9cd5a9ab9b936388e3c4f11e5d44e175eb7))
# [0.32.0](https://github.com/protectwise/troika/compare/v0.31.0...v0.32.0) (2020-09-16)
### Bug Fixes
* **troika-three-utils:** make derived material methods writable+configurable ([4d4bfbc](https://github.com/protectwise/troika/commit/4d4bfbc5d4d730eb0098d33beb1c3c562037fddf)), closes [react-spring/drei#121](https://github.com/react-spring/drei/issues/121)
# [0.31.0](https://github.com/protectwise/troika/compare/v0.30.2...v0.31.0) (2020-08-11)
### Bug Fixes
* move Three.js peerDependency from troika-3d to troika-three-utils ([96b6fc8](https://github.com/protectwise/troika/commit/96b6fc8c9d80ddce358b74d6bb80a063500476ac))
# [0.30.0](https://github.com/protectwise/troika/compare/v0.29.0...v0.30.0) (2020-07-16)
**Note:** Version bump only for package troika-three-utils
# [0.29.0](https://github.com/protectwise/troika/compare/v0.28.1...v0.29.0) (2020-07-06)
### Bug Fixes
* **troika-three-utils:** fix program switching when double-deriving materials ([89ed2f8](https://github.com/protectwise/troika/commit/89ed2f8eab6dfccba3aab95ea667642d13976bfc))
## [0.28.1](https://github.com/protectwise/troika/compare/v0.28.0...v0.28.1) (2020-06-17)
### Bug Fixes
* **troika-3d-text:** don't dispose derived materials on base material switch ([3d88475](https://github.com/protectwise/troika/commit/3d88475d2f3ead6bec92694ff0720d4ab643e872)), closes [#59](https://github.com/protectwise/troika/issues/59)
### Performance Improvements
* **troika-three-utils:** increase chance of program reuse in createDerivedMaterial ([56daf65](https://github.com/protectwise/troika/commit/56daf6535a7bd8fec30d86a713feb5b8f26fa6a5)), closes [#59](https://github.com/protectwise/troika/issues/59)
# [0.28.0](https://github.com/protectwise/troika/compare/v0.27.1...v0.28.0) (2020-06-09)
**Note:** Version bump only for package troika-three-utils
# [0.27.0](https://github.com/protectwise/troika/compare/v0.26.1...v0.27.0) (2020-06-02)
**Note:** Version bump only for package troika-three-utils
# [0.26.0](https://github.com/protectwise/troika/compare/v0.25.0...v0.26.0) (2020-05-24)
**Note:** Version bump only for package troika-three-utils
# [0.25.0](https://github.com/protectwise/troika/compare/v0.24.1...v0.25.0) (2020-05-19)
**Note:** Version bump only for package troika-three-utils
# [0.24.0](https://github.com/protectwise/troika/compare/v0.23.0...v0.24.0) (2020-04-27)
### Bug Fixes
* **troika-three-utils:** allow use of the timeUniform within glsl functions ([7354b9e](https://github.com/protectwise/troika/commit/7354b9ea03d7ffedd869ccc4bb496811a572deba))
# [0.23.0](https://github.com/protectwise/troika/compare/v0.22.0...v0.23.0) (2020-04-16)
### Bug Fixes
* **troika-three-utils:** fragmentColorTransform is now inserted prior to postprocessing chunks ([97cd9ac](https://github.com/protectwise/troika/commit/97cd9ac3276c353597a374f1c7d2c7f23993aa7f)), closes [#20](https://github.com/protectwise/troika/issues/20)
# [0.22.0](https://github.com/protectwise/troika/compare/v0.21.0...v0.22.0) (2020-04-02)
**Note:** Version bump only for package troika-three-utils
# [0.21.0](https://github.com/protectwise/troika/compare/v0.20.0...v0.21.0) (2020-03-27)
### Features
* **examples:** beziers: add instanceable version of the beziers, and add point light option ([0739f4d](https://github.com/protectwise/troika/commit/0739f4d6ebcdd13be46b6371c95504b290c86359))
* **troika-three-utils:** added new options for createDerivedMaterial ([d67bb4a](https://github.com/protectwise/troika/commit/d67bb4a569e151efad87a047e845607226f02027))
* **troika-three-utils:** derived shadow material uniforms are now synced automatically ([7843f23](https://github.com/protectwise/troika/commit/7843f2314caf9463262a16b15de948931b4b6511))
# [0.20.0](https://github.com/protectwise/troika/compare/v0.19.0...v0.20.0) (2020-03-16)
**Note:** Version bump only for package troika-three-utils
# [0.19.0](https://github.com/protectwise/troika/compare/v0.19.0-alpha.0...v0.19.0) (2020-02-28)
**Note:** Version bump only for package troika-three-utils
# [0.18.0](https://github.com/protectwise/troika/compare/v0.17.1...v0.18.0) (2020-02-21)
**Note:** Version bump only for package troika-three-utils
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+44
View File
@@ -0,0 +1,44 @@
# Troika Three.js Utilities
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 on Three.js versions in the Getting Started docs](../../docs/getting-started/setup.md#threejs) for details.
```sh
npm install three
```
## 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()
This utility allows you to easily _extend_ existing Three.js materials with your own custom shader code. This is an incredibly powerful tool, and is the secret behind most of Troika's shader-driven tools like `troika-three-text`, `three-instanced-uniforms-mesh`, and `BezierMesh`.
See the [createDerivedMaterial documentation page](./docs/createDerivedMaterial.md) and its [source code with JSDoc](./src/DerivedMaterial.js) for details.
### BezierMesh
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.
See the [BezierMesh documentation page](./docs/BezierMesh.md) and its [source code with JSDoc](./src/BezierMesh.js) for details.
### InstancedUniformsMesh
> NOTE: InstancedUniformsMesh has been moved to [its own `three-instanced-uniforms-mesh` package](https://github.com/protectwise/troika/tree/master/packages/three-instanced-uniforms-mesh).
+755
View File
@@ -0,0 +1,755 @@
import { ShaderChunk, UniformsUtils, MeshDepthMaterial, RGBADepthPacking, MeshDistanceMaterial, ShaderLib, Matrix4, Vector3, Mesh, CylinderGeometry, 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 (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.
*/
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
}
// 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]|mat[34])\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 CylinderGeometry(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()
}
set customDepthMaterial(m) {
// future: let the user override with their own?
}
get customDistanceMaterial() {
return this.material.getDistanceMaterial()
}
set customDistanceMaterial(m) {
// future: let the user override with their own?
}
onBeforeRender() {
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 };
+769
View File
@@ -0,0 +1,769 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three')) :
typeof define === 'function' && define.amd ? define(['exports', 'three'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.troika_three_utils = {}, global.THREE));
}(this, (function (exports, three) { 'use strict';
/**
* 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 = three.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 (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.
*/
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, three.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 three.MeshDepthMaterial({ depthPacking: three.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 three.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
}
// 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 ? three.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]|mat[34])\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 three.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 three.Vector3()},
controlA: {value: new three.Vector3()},
controlB: {value: new three.Vector3()},
pointB: {value: new three.Vector3()},
radius: {value: 0.01},
dashing: {value: new three.Vector3()} //on, off, offset
},
vertexDefs,
vertexTransform,
fragmentDefs,
fragmentMainIntro
}
)
}
let geometry = null;
const defaultBaseMaterial = /*#__PURE__*/new three.MeshStandardMaterial({color: 0xffffff, side: three.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 three.Mesh {
static getGeometry() {
return geometry || (geometry =
new three.CylinderGeometry(1, 1, 1, 6, 64).translate(0, 0.5, 0)
)
}
constructor() {
super(
BezierMesh.getGeometry(),
defaultBaseMaterial
);
this.pointA = new three.Vector3();
this.controlA = new three.Vector3();
this.controlB = new three.Vector3();
this.pointB = new three.Vector3();
this.radius = 0.01;
this.dashArray = new three.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()
}
set customDepthMaterial(m) {
// future: let the user override with their own?
}
get customDistanceMaterial() {
return this.material.getDistanceMaterial()
}
set customDistanceMaterial(m) {
// future: let the user override with their own?
}
onBeforeRender() {
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
}
}
exports.BezierMesh = BezierMesh;
exports.createDerivedMaterial = createDerivedMaterial;
exports.expandShaderIncludes = expandShaderIncludes;
exports.getShaderUniformTypes = getShaderUniformTypes;
exports.getShadersForMaterial = getShadersForMaterial;
exports.invertMatrix4 = invertMatrix4;
exports.voidMainRegExp = voidMainRegExp;
Object.defineProperty(exports, '__esModule', { value: true });
})));
+36
View File
@@ -0,0 +1,36 @@
'use strict';(function(h,c){"object"===typeof exports&&"undefined"!==typeof module?c(exports,require("three")):"function"===typeof define&&define.amd?define(["exports","three"],c):(h="undefined"!==typeof globalThis?globalThis:h||self,c(h.troika_three_utils={},h.THREE))})(this,function(h,c){function q(a){return a.replace(/^[ \t]*#include +<([\w\d./]+)>/gm,function(a,k){return(k=c.ShaderChunk[k])?q(k):a})}function m(a,b){let k=D(b),e=n.get(a);e||n.set(a,e=Object.create(null));if(e[k])return new e[k];
let g=`_onBeforeCompile${k}`,h=function(d,e){a.onBeforeCompile.call(this,d,e);e=this.customProgramCacheKey()+"|"+d.vertexShader+"|"+d.fragmentShader;var c=v[e];c||(c=E(this,d,b,k),c=v[e]=c);d.vertexShader=c.vertexShader;d.fragmentShader=c.fragmentShader;l(d.uniforms,this.uniforms);b.timeUniform&&(d.uniforms[b.timeUniform]={get value(){return Date.now()-F}});if(this[g])this[g](d)},t=function(){return w(b.chained?a:a.clone())},w=function(d){const c=Object.create(d,G);Object.defineProperty(c,"baseMaterial",
{value:a});Object.defineProperty(c,"id",{value:H++});{var e=4294967295*Math.random()|0;let a=4294967295*Math.random()|0,b=4294967295*Math.random()|0,d=4294967295*Math.random()|0;e=(f[e&255]+f[e>>8&255]+f[e>>16&255]+f[e>>24&255]+"-"+f[a&255]+f[a>>8&255]+"-"+f[a>>16&15|64]+f[a>>24&255]+"-"+f[b&63|128]+f[b>>8&255]+"-"+f[b>>16&255]+f[b>>24&255]+f[d&255]+f[d>>8&255]+f[d>>16&255]+f[d>>24&255]).toUpperCase()}c.uuid=e;c.uniforms=l({},d.uniforms,b.uniforms);c.defines=l({},d.defines,b.defines);c.defines[`TROIKA_DERIVED_MATERIAL_${k}`]=
"";c.extensions=l({},d.extensions,b.extensions);c._listeners=void 0;return c},G={constructor:{value:t},isDerivedMaterial:{value:!0},type:{get:()=>a.type,set:b=>{a.type=b}},isDerivedFrom:{writable:!0,configurable:!0,value:function(a){const b=this.baseMaterial;return a===b||b.isDerivedMaterial&&b.isDerivedFrom(a)||!1}},customProgramCacheKey:{writable:!0,configurable:!0,value:function(){return a.customProgramCacheKey()+"|"+k}},onBeforeCompile:{get(){return h},set(a){this[g]=a}},copy:{writable:!0,configurable:!0,
value:function(b){a.copy.call(this,b);a.isShaderMaterial||a.isDerivedMaterial||(l(this.extensions,b.extensions),l(this.defines,b.defines),l(this.uniforms,c.UniformsUtils.clone(b.uniforms)));return this}},clone:{writable:!0,configurable:!0,value:function(){const b=new a.constructor;return w(b).copy(this)}},getDepthMaterial:{writable:!0,configurable:!0,value:function(){let d=this._depthMaterial;d||(d=this._depthMaterial=m(a.isDerivedMaterial?a.getDepthMaterial():new c.MeshDepthMaterial({depthPacking:c.RGBADepthPacking}),
b),d.defines.IS_DEPTH_MATERIAL="",d.uniforms=this.uniforms);return d}},getDistanceMaterial:{writable:!0,configurable:!0,value:function(){let d=this._distanceMaterial;d||(d=this._distanceMaterial=m(a.isDerivedMaterial?a.getDistanceMaterial():new c.MeshDistanceMaterial,b),d.defines.IS_DISTANCE_MATERIAL="",d.uniforms=this.uniforms);return d}},dispose:{writable:!0,configurable:!0,value(){const {_depthMaterial:b,_distanceMaterial:c}=this;b&&b.dispose();c&&c.dispose();a.dispose.call(this)}}};e[k]=t;return new t}
function E(a,{vertexShader:b,fragmentShader:c},e,g){let {vertexDefs:k,vertexMainIntro:f,vertexMainOutro:h,vertexTransform:l,fragmentDefs:d,fragmentMainIntro:m,fragmentMainOutro:p,fragmentColorTransform:u,customRewriter:r,timeUniform:n}=e;k=k||"";f=f||"";h=h||"";d=d||"";m=m||"";p=p||"";if(l||r)b=q(b);if(u||r)c=c.replace(/^[ \t]*#include <((?:tonemapping|encodings|colorspace|fog|premultiplied_alpha|dithering)_fragment)>/gm,"\n//!BEGIN_POST_CHUNK $1\n$&\n//!END_POST_CHUNK\n"),c=q(c);r&&(c=r({vertexShader:b,
fragmentShader:c}),b=c.vertexShader,c=c.fragmentShader);if(u){let a=[];c=c.replace(/^\/\/!BEGIN_POST_CHUNK[^]+?^\/\/!END_POST_CHUNK/gm,b=>{a.push(b);return""});p=`${u}\n${a.join("\n")}\n${p}`}n&&(e=`\nuniform float ${n};\n`,k=e+k,d=e+d);l&&(k=`${k}
void troikaVertexTransform${g}(inout vec3 position, inout vec3 normal, inout vec2 uv) {
${l}
}
`,f=`
troika_position_${g} = vec3(position);
troika_normal_${g} = vec3(normal);
troika_uv_${g} = vec2(uv);
troikaVertexTransform${g}(troika_position_${g}, troika_normal_${g}, troika_uv_${g});
${f}
`,b=`vec3 troika_position_${g};
vec3 troika_normal_${g};
vec2 troika_uv_${g};
${b}
`.replace(/\b(position|normal|uv)\b/g,(a,b,c,e)=>/\battribute\s+vec[23]\s+$/.test(e.substr(0,c))?b:`troika_${b}_${g}`),a.map&&0<a.map.channel||(b=b.replace(/\bMAP_UV\b/g,`troika_uv_${g}`)));b=x(b,g,k,f,h);c=x(c,g,d,m,p);return{vertexShader:b,fragmentShader:c}}function x(a,b,c,e,g){if(e||g||c)a=a.replace(y,`
${c}
void troikaOrigMain${b}() {`),a+=`
void main() {
${e}
troikaOrigMain${b}();
${g}
}`;return a}function I(a,b){return"uniforms"===a?void 0:"function"===typeof b?b.toString():b}function D(a){a=JSON.stringify(a,I);let b=z.get(a);null==b&&z.set(a,b=++J);return b}let y=/\bvoid\s+main\s*\(\s*\)\s*{/g,f=[];for(let a=0;256>a;a++)f[a]=(16>a?"0":"")+a.toString(16);let l=Object.assign||function(){let a=arguments[0];for(let b=1,c=arguments.length;b<c;b++){let c=arguments[b];if(c)for(let b in c)Object.prototype.hasOwnProperty.call(c,b)&&(a[b]=c[b])}return a},F=Date.now(),n=new WeakMap,v=new Map,
H=1E10,J=0,z=new Map,K={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"},A=null,B=new c.MeshStandardMaterial({color:16777215,side:c.DoubleSide});
class C extends c.Mesh{static getGeometry(){return A||(A=(new c.CylinderGeometry(1,1,1,6,64)).translate(0,.5,0))}constructor(){super(C.getGeometry(),B);this.pointA=new c.Vector3;this.controlA=new c.Vector3;this.controlB=new c.Vector3;this.pointB=new c.Vector3;this.radius=.01;this.dashArray=new c.Vector2;this.dashOffset=0;this.frustumCulled=!1}get material(){let a=this._derivedMaterial,b=this._baseMaterial||this._defaultMaterial||(this._defaultMaterial=B.clone());a&&a.baseMaterial===b||(a=this._derivedMaterial=
m(b,{chained:!0,uniforms:{pointA:{value:new c.Vector3},controlA:{value:new c.Vector3},controlB:{value:new c.Vector3},pointB:{value:new c.Vector3},radius:{value:.01},dashing:{value:new c.Vector3}},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",
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',
fragmentDefs:"\nuniform vec3 dashing;\nvarying float bezierT;\n",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"}),b.addEventListener("dispose",function e(){b.removeEventListener("dispose",e);a.dispose()}));return a}set material(a){this._baseMaterial=a}get customDepthMaterial(){return this.material.getDepthMaterial()}set customDepthMaterial(a){}get customDistanceMaterial(){return this.material.getDistanceMaterial()}set customDistanceMaterial(a){}onBeforeRender(){let {uniforms:a}=
this.material,{pointA:b,controlA:c,controlB:e,pointB:g,radius:f,dashArray:h,dashOffset:l}=this;a.pointA.value.copy(b);a.controlA.value.copy(c);a.controlB.value.copy(e);a.pointB.value.copy(g);a.radius.value=f;a.dashing.value.set(h.x,h.y,l||0)}raycast(){}}h.BezierMesh=C;h.createDerivedMaterial=m;h.expandShaderIncludes=q;h.getShaderUniformTypes=function(a){let b=/\buniform\s+(int|float|vec[234]|mat[34])\s+([A-Za-z_][\w]*)/g,c=Object.create(null),e;for(;null!==(e=b.exec(a));)c[e[2]]=e[1];return c};h.getShadersForMaterial=
function(a){let b=K[a.type];return b?c.ShaderLib[b]:a};h.invertMatrix4=function(a,b=new c.Matrix4){"function"===typeof b.invert?b.copy(a).invert():b.getInverse(a);return b};h.voidMainRegExp=y;Object.defineProperty(h,"__esModule",{value:!0})})
+57
View File
@@ -0,0 +1,57 @@
# BezierMesh
This is a Three.js object which bends a cylindrical mesh along a 3D cubic bezier path between two points. This is useful for drawing nicely curved lines in 3D space, where the lines have thickness.
Rather than assembling a BufferGeometry on the CPU, BezierMesh bends the tube on the GPU in a custom derived vertex shader. This makes it very good for situations where the line's endpoints and control points change over time. They can even be animated every frame without penalty.
It can also have any `material` assigned to it, so it can have lighting, textures, etc. like any other mesh. It will automatically upgrade that material behind the scenes to apply the extra vertex shader transformation.
- _[Source code with JSDoc](https://github.com/protectwise/troika/blob/master/packages/troika-three-utils/src/BezierMesh.js)_
- _[Online example](https://troika-examples.netlify.com/#bezier3d)_
![Example 1](../../../docs/troika-three-utils/images/beziers1.png)
- _[Online example using InstancedUniformsMesh](https://ibyou.csb.app/)_
![Example 2](../../../docs/troika-three-utils/images/beziers2.png)
## Usage:
```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)
```
## Supported Properties:
### `pointA`
A Vector3 holding the position of the first endpoint.
### `controlA`
A Vector3 holding the position of the first control point.
### `controlB`
A Vector3 holding the position of the second control point.
### `pointB`
A Vector3 holding the position of the second endpoint.
### `radius`
A number defining the radius of the tube.
### `dashArray`
An array of two numbers, defining the length of "on" and "off" parts of a dashed line style. Each number 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.)
> Note that the dashes will appear like a hollow tube, not solid; this will be more apparent on thicker tubes.
### `dashOffset`
A numeric offset of where the dash starts. You can animate this to make the dashes move.
+49
View File
@@ -0,0 +1,49 @@
# Three.js Derived Materials
**How to use Troika's `createDerivedMaterial` utility to extend existing Three.js materials with custom shader code**
_[Source code with JSDoc](https://github.com/protectwise/troika/blob/master/packages/troika-three-utils/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 can be 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.
Lastly, you can create a derived material from _another derived material_, and so on. This enables composable patterns where you can piece in small bits of shader logic one at a time.
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, PlaneGeometry } from 'three'
const baseMaterial = new MeshStandardMaterial({color: 0xffcc00})
const customMaterial = createDerivedMaterial(
baseMaterial,
{
timeUniform: 'elapsed',
// Add GLSL to tweak the vertex... notice this modifies the `position`
// and `normal` attributes, which is normally not possible!
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 PlaneGeometry(1, 1, 64, 1),
customMaterial
)
// to enable directional light shadows:
mesh.castShadow = true
mesh.customDepthMaterial = customMaterial.getDepthMaterial()
```
You can also declare custom `uniforms` and `defines`, inject fragment shader code to modify the output color, etc. See the JSDoc in the [DerivedMaterial.js source code](https://github.com/protectwise/troika/blob/master/packages/troika-three-utils/src/DerivedMaterial.js) for full details.
+20
View File
@@ -0,0 +1,20 @@
{
"name": "troika-three-utils",
"version": "0.52.4",
"description": "Various utilities related to Three.js",
"author": "Jason Johnston <jason.johnston@protectwise.com>",
"repository": {
"type": "git",
"url": "https://github.com/protectwise/troika.git",
"directory": "packages/troika-three-utils"
},
"license": "MIT",
"sideEffects": false,
"main": "dist/troika-three-utils.umd.js",
"module": "dist/troika-three-utils.esm.js",
"module:src": "src/index.js",
"peerDependencies": {
"three": ">=0.125.0"
},
"gitHead": "728a1780127df0c2509e967b03f17ddc46bbf5d2"
}
+112
View File
@@ -0,0 +1,112 @@
import { CylinderGeometry, DoubleSide, Mesh, MeshStandardMaterial, Vector2, Vector3 } from 'three'
import { createBezierMeshMaterial } from './BezierMeshMaterial.js'
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 CylinderGeometry(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()
}
set customDepthMaterial(m) {
// future: let the user override with their own?
}
get customDistanceMaterial() {
return this.material.getDistanceMaterial()
}
set customDistanceMaterial(m) {
// future: let the user override with their own?
}
onBeforeRender() {
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 }
+110
View File
@@ -0,0 +1,110 @@
import { createDerivedMaterial } from './DerivedMaterial.js'
import { Vector3 } from 'three'
/*
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);
// `
export 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
}
)
}
+426
View File
@@ -0,0 +1,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
}
+17
View File
@@ -0,0 +1,17 @@
import { ShaderChunk } from 'three'
/**
* 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
*/
export 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 )
}
+29
View File
@@ -0,0 +1,29 @@
/*
* 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)
}
export 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()
}
+16
View File
@@ -0,0 +1,16 @@
/**
* 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
*/
export function getShaderUniformTypes(shader) {
let uniformRE = /\buniform\s+(int|float|vec[234]|mat[34])\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
}
+34
View File
@@ -0,0 +1,34 @@
import { ShaderLib } from 'three'
// 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:''}`
*/
export function getShadersForMaterial(material) {
let builtinType = MATERIAL_TYPES_TO_SHADERS[material.type]
return builtinType ? ShaderLib[builtinType] : material //TODO fallback for unknown type?
}
+9
View File
@@ -0,0 +1,9 @@
// Troika Three.js Utilities exports
export { createDerivedMaterial } from './DerivedMaterial.js'
export { getShadersForMaterial } from './getShadersForMaterial.js'
export { getShaderUniformTypes } from './getShaderUniformTypes.js'
export { expandShaderIncludes } from './expandShaderIncludes.js'
export { invertMatrix4 } from './invertMatrix4.js'
export { voidMainRegExp } from './voidMainRegExp.js'
export { BezierMesh } from './BezierMesh.js'
+16
View File
@@ -0,0 +1,16 @@
import { Matrix4 } from 'three'
/**
* Helper for smoothing out the `m.getInverse(x)` --> `m.copy(x).invert()` conversion
* that happened in ThreeJS r123.
* @param {Matrix4} srcMatrix
* @param {Matrix4} [tgtMatrix]
*/
export function invertMatrix4(srcMatrix, tgtMatrix = new Matrix4()) {
if (typeof tgtMatrix.invert === 'function') {
tgtMatrix.copy(srcMatrix).invert()
} else {
tgtMatrix.getInverse(srcMatrix)
}
return tgtMatrix
}
+5
View File
@@ -0,0 +1,5 @@
/**
* Regular expression for matching the `void main() {` opener line in GLSL.
* @type {RegExp}
*/
export const voidMainRegExp = /\bvoid\s+main\s*\(\s*\)\s*{/g