Fold NPCFACTORY in: vendor three.js, take the banks, and fix units without flattening body types

NPCFACTORY and wardrobegod were the same product — wardrobegod had already absorbed its reskin
engine, and keeping two benches means two half-libraries and two export paths. Folded, keeping
the wardrobegod name and codebase (370 lines there vs ~1,100 here).

· Vendored three.js r175 from NPCFACTORY, replacing the unpkg CDN importmap. This was real
  drift, not tidiness: a CDN import breaks offline and can't be lifted into a game build.
  NPCFACTORY lacked OrbitControls (it used PointerLock), so that one addon was fetched at the
  matching revision. Added a guarded /vendor/ static route. Verified in-browser: zero CDN
  requests, 5 vendor files, model still loads.
· Took the 17 rigged walk-animated NPCs and 6 parts. Bodies 4 -> 20.
· New `unitfix` op. Deliberately NOT scale-to-height: a 0.06m human is a UNIT error, but
  normalising everything to 1.72m would erase the small/medium/large/obese range the library is
  meant to carry. So it only corrects heights outside 0.5-3.0m — physically impossible for a
  human — and leaves real proportions alone as data. Verified both ways: hum_character 0.0576m
  -> 1.72m, tradie 1.000m left untouched.

Three bugs found while building it, two of them pre-existing:
· `is_helper`/`real_meshes`/`bbox_of` factored out. Material-less bone widgets (a radius-1
  42-vert Icosphere, so exactly 2.0 units tall) were being measured INSTEAD of the character —
  every body reported 2.000m. This poisoned the `scale` op too, which has been measuring
  widgets all along; NPCFACTORY's render_plates.py had independently worked around the same
  thing by framing on the dominant mesh.
· `transform_apply` under temp_override(selected_editable_objects=...) SEGFAULTS Blender 5.1.2
  on rigs with parented children. A segfault can't be caught, so it's avoided rather than
  handled: glTF encodes node scale natively, so setting the root transform is sufficient and
  every downstream measurement still reads correctly. Confirmed `scale` still round-trips
  (1.00m -> 1.72m, re-measured).
· My own bulk edit replaced only ONE of the two crash-prone call sites and reported "replaced 1"
  — I didn't check for a second, which is why `scale` worked while `unitfix` kept crashing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-24 19:36:03 +10:00
parent 8119e2de4c
commit 037f6ef0af
21 changed files with 86409 additions and 11 deletions

View File

@ -62,6 +62,54 @@ def meshes(objs):
return [o for o in objs if o.type == 'MESH']
def is_helper(o):
"""Bone-widget / marker mesh: no material and a handful of verts.
These ride along in character_kit and NPCFACTORY rigs (a radius-1 42-vert 'Icosphere', so
exactly 2.0 units tall) and they poison every bbox measurement height readouts, camera
auto-framing, and scale normalisation all silently measure the widget instead of the
character. NPCFACTORY's render_plates.py hit this too and worked around it by framing on
the dominant mesh only.
"""
return (o.type == 'MESH' and not [m for m in o.data.materials if m]
and len(o.data.vertices) < 100)
def real_meshes(objs):
"""Meshes that are actually the model — helpers excluded. Falls back to all meshes so a
genuinely material-less model still measures rather than returning nothing."""
ms = meshes(objs)
return [o for o in ms if not is_helper(o)] or ms
def apply_scale(roots, objs):
"""Scale a model by setting the ROOT transform — deliberately without transform_apply.
Blender 5.1.2 SEGFAULTS inside object_transform_apply_exec when the selection contains both
an armature and its own parented children (reproducible on character_kit's hum_character,
both with temp_override and with a plain select-then-apply). A segfault kills the process, so
it has to be avoided rather than caught.
Not baking costs us nothing here: glTF encodes node scale natively, so the exported file is
the right size and every downstream measurement (bbox, height, the viewer) reads it
correctly. Only the roots are scaled children inherit, and scaling them too would
double-apply.
"""
# The caller has already set r.scale; there is deliberately nothing else to do. An earlier
# view_layer.update() here was itself crashing on rigs with parented children.
return
def bbox_of(objs):
import mathutils
mn = mathutils.Vector((1e9,) * 3); mx = mathutils.Vector((-1e9,) * 3)
for o in real_meshes(objs):
for c in o.bound_box:
w = o.matrix_world @ mathutils.Vector(c)
mn = mathutils.Vector(map(min, mn, w)); mx = mathutils.Vector(map(max, mx, w))
return mn, mx
def armatures(objs):
return [o for o in objs if o.type == 'ARMATURE']
@ -76,12 +124,7 @@ if OP == 'convert':
elif OP == 'scale':
clean()
objs = load(ARGS[0]); target = float(ARGS[2])
import mathutils
mn = mathutils.Vector((1e9,) * 3); mx = mathutils.Vector((-1e9,) * 3)
for o in meshes(objs):
for c in o.bound_box:
w = o.matrix_world @ mathutils.Vector(c)
mn = mathutils.Vector(map(min, mn, w)); mx = mathutils.Vector(map(max, mx, w))
mn, mx = bbox_of(objs) # same helper-widget exclusion — this op measured them too
h = mx.z - mn.z
if h <= 0:
raise SystemExit('flat object — no height to scale')
@ -89,8 +132,7 @@ elif OP == 'scale':
roots = [o for o in objs if not o.parent]
for r in roots:
r.scale = [c * s for c in r.scale]
with bpy.context.temp_override(selected_editable_objects=roots + meshes(objs) + armatures(objs)):
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
apply_scale(roots, objs)
print(f'scaled ×{s:.3f}{target}m')
export(ARGS[1])
@ -146,6 +188,35 @@ elif OP == 'fit':
print(f'fitted {len(meshes(garm_objs))} garment mesh(es), mode={mode}, inflate={inflate * 1000:.0f}mm')
export(ARGS[2])
elif OP == 'unitfix':
# unitfix <in> <out.glb> [target_m]
#
# Deliberately NOT scale-to-height. A 0.06m human is a UNIT error (character_kit rigs are
# 6cm, NPCFACTORY banks 1.0m, TRELLIS output varies) and silently breaks assemble — that's
# what produced a 27x oversized garment. But normalising every body to one height would
# erase the small/medium/large/obese range the library is meant to carry, so we only correct
# scales that are physically impossible for a human and leave real proportions alone.
clean()
objs = load(ARGS[0])
target = float(ARGS[2]) if len(ARGS) > 2 else 1.72
mn, mx = bbox_of(objs) # helper widgets excluded, or we'd measure a 2.0-unit Icosphere
h = mx.z - mn.z
if h <= 0:
raise SystemExit('flat object — no height to measure')
PLAUSIBLE = (0.5, 3.0) # any human outside this is a broken unit scale, not a body type
if PLAUSIBLE[0] <= h <= PLAUSIBLE[1]:
print(f'height {h:.3f}m is plausible — left alone (body-type variation is data, not an error)')
s = 1.0
else:
s = target / h
roots = [o for o in objs if not o.parent]
for r in roots:
r.scale = [c * s for c in r.scale]
apply_scale(roots, objs)
print(f'UNIT FIX: {h:.4f}m is impossible for a human -> scaled x{s:.4f} to {target}m')
print(f'unitfix done (scale {s:.4f})')
export(ARGS[1])
elif OP == 'harvest':
# harvest <donor> <out.glb> <material_substr> [keep_skin_regex]
#
@ -290,7 +361,7 @@ elif OP == 'assemble':
# scene bbox — which drives both the HUD height and the viewer's auto-framing, so one stray
# widget makes the camera frame the widget and the character render as a speck.
for o in list(bpy.data.objects):
if o.type == 'MESH' and not [m for m in o.data.materials if m] and len(o.data.vertices) < 100:
if is_helper(o):
print(f' dropped helper mesh {o.name} ({len(o.data.vertices)} verts, no material)')
bpy.data.objects.remove(o, do_unlink=True)
print(f'assembled body + {len(ARGS) - 2} garment file(s)')

View File

@ -304,6 +304,13 @@ class H(BaseHTTPRequestHandler):
q = dict(urllib.parse.parse_qsl(u.query))
if u.path == '/':
return self.send(200, open(os.path.join(ROOT, 'web', 'index.html'), 'rb').read(), 'text/html')
if u.path.startswith('/vendor/'): # vendored three.js — no CDN, works offline
f = os.path.realpath(os.path.join(ROOT, 'web', u.path.lstrip('/')))
web = os.path.realpath(os.path.join(ROOT, 'web'))
if not f.startswith(web + os.sep) or not os.path.isfile(f):
return self.j({'error': 'not found'}, 404)
ctype = 'text/javascript' if f.endswith('.js') else (mimetypes.guess_type(f)[0] or 'application/octet-stream')
return self.send(200, open(f, 'rb').read(), ctype)
if u.path == '/api/lib':
return self.j({'lib': scan(), 'mb': bool(MB_TOKEN), 'cf': bool(CF_ACCT and CF_TOKEN),
'blender': os.path.exists(BLENDER), 'doll': doll_catalogue()})

View File

@ -203,10 +203,13 @@
<div id="foot"><span id="job">ready.</span></div>
</div>
<!-- three.js is VENDORED (r175), not CDN. The bench has to work offline, and anything built
against a CDN import can't be lifted into a game build. Folded in from NPCFACTORY, which
already had this right; OrbitControls was the one addon it lacked (it used PointerLock). -->
<script type="importmap">
{ "imports": {
"three": "https://unpkg.com/three@0.175.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.175.0/examples/jsm/"
"three": "./vendor/three.module.js",
"three/addons/": "./vendor/addons/"
}}
</script>
<script type="module">

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,270 @@
import {
Controls,
Euler,
Vector3
} from 'three';
const _euler = new Euler( 0, 0, 0, 'YXZ' );
const _vector = new Vector3();
/**
* Fires when the user moves the mouse.
*
* @event PointerLockControls#change
* @type {Object}
*/
const _changeEvent = { type: 'change' };
/**
* Fires when the pointer lock status is "locked" (in other words: the mouse is captured).
*
* @event PointerLockControls#lock
* @type {Object}
*/
const _lockEvent = { type: 'lock' };
/**
* Fires when the pointer lock status is "unlocked" (in other words: the mouse is not captured anymore).
*
* @event PointerLockControls#unlock
* @type {Object}
*/
const _unlockEvent = { type: 'unlock' };
const _PI_2 = Math.PI / 2;
/**
* The implementation of this class is based on the [Pointer Lock API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API}.
* `PointerLockControls` is a perfect choice for first person 3D games.
*
* ```js
* const controls = new PointerLockControls( camera, document.body );
*
* // add event listener to show/hide a UI (e.g. the game's menu)
* controls.addEventListener( 'lock', function () {
*
* menu.style.display = 'none';
*
* } );
*
* controls.addEventListener( 'unlock', function () {
*
* menu.style.display = 'block';
*
* } );
* ```
*
* @augments Controls
*/
class PointerLockControls extends Controls {
/**
* Constructs a new controls instance.
*
* @param {Camera} camera - The camera that is managed by the controls.
* @param {?HTMLDOMElement} domElement - The HTML element used for event listeners.
*/
constructor( camera, domElement = null ) {
super( camera, domElement );
/**
* Whether the controls are locked or not.
*
* @type {boolean}
* @readonly
* @default false
*/
this.isLocked = false;
/**
* Camera pitch, lower limit. Range is '[0, Math.PI]' in radians.
*
* @type {number}
* @default 0
*/
this.minPolarAngle = 0;
/**
* Camera pitch, upper limit. Range is '[0, Math.PI]' in radians.
*
* @type {number}
* @default Math.PI
*/
this.maxPolarAngle = Math.PI;
/**
* Multiplier for how much the pointer movement influences the camera rotation.
*
* @type {number}
* @default 1
*/
this.pointerSpeed = 1.0;
// event listeners
this._onMouseMove = onMouseMove.bind( this );
this._onPointerlockChange = onPointerlockChange.bind( this );
this._onPointerlockError = onPointerlockError.bind( this );
if ( this.domElement !== null ) {
this.connect( this.domElement );
}
}
connect( element ) {
super.connect( element );
this.domElement.ownerDocument.addEventListener( 'mousemove', this._onMouseMove );
this.domElement.ownerDocument.addEventListener( 'pointerlockchange', this._onPointerlockChange );
this.domElement.ownerDocument.addEventListener( 'pointerlockerror', this._onPointerlockError );
}
disconnect() {
this.domElement.ownerDocument.removeEventListener( 'mousemove', this._onMouseMove );
this.domElement.ownerDocument.removeEventListener( 'pointerlockchange', this._onPointerlockChange );
this.domElement.ownerDocument.removeEventListener( 'pointerlockerror', this._onPointerlockError );
}
dispose() {
this.disconnect();
}
getObject() {
console.warn( 'THREE.PointerLockControls: getObject() has been deprecated. Use controls.object instead.' ); // @deprecated r169
return this.object;
}
/**
* Returns the look direction of the camera.
*
* @param {Vector3} v - The target vector that is used to store the method's result.
* @return {Vector3} The normalized direction vector.
*/
getDirection( v ) {
return v.set( 0, 0, - 1 ).applyQuaternion( this.object.quaternion );
}
/**
* Moves the camera forward parallel to the xz-plane. Assumes camera.up is y-up.
*
* @param {number} distance - The signed distance.
*/
moveForward( distance ) {
if ( this.enabled === false ) return;
// move forward parallel to the xz-plane
// assumes camera.up is y-up
const camera = this.object;
_vector.setFromMatrixColumn( camera.matrix, 0 );
_vector.crossVectors( camera.up, _vector );
camera.position.addScaledVector( _vector, distance );
}
/**
* Moves the camera sidewards parallel to the xz-plane.
*
* @param {number} distance - The signed distance.
*/
moveRight( distance ) {
if ( this.enabled === false ) return;
const camera = this.object;
_vector.setFromMatrixColumn( camera.matrix, 0 );
camera.position.addScaledVector( _vector, distance );
}
/**
* Activates the pointer lock.
*
* @param {boolean} [unadjustedMovement=false] - Disables OS-level adjustment for mouse acceleration, and accesses raw mouse input instead.
* Setting it to true will disable mouse acceleration.
*/
lock( unadjustedMovement = false ) {
this.domElement.requestPointerLock( {
unadjustedMovement
} );
}
/**
* Exits the pointer lock.
*/
unlock() {
this.domElement.ownerDocument.exitPointerLock();
}
}
// event listeners
function onMouseMove( event ) {
if ( this.enabled === false || this.isLocked === false ) return;
const camera = this.object;
_euler.setFromQuaternion( camera.quaternion );
_euler.y -= event.movementX * 0.002 * this.pointerSpeed;
_euler.x -= event.movementY * 0.002 * this.pointerSpeed;
_euler.x = Math.max( _PI_2 - this.maxPolarAngle, Math.min( _PI_2 - this.minPolarAngle, _euler.x ) );
camera.quaternion.setFromEuler( _euler );
this.dispatchEvent( _changeEvent );
}
function onPointerlockChange() {
if ( this.domElement.ownerDocument.pointerLockElement === this.domElement ) {
this.dispatchEvent( _lockEvent );
this.isLocked = true;
} else {
this.dispatchEvent( _unlockEvent );
this.isLocked = false;
}
}
function onPointerlockError() {
console.error( 'THREE.PointerLockControls: Unable to use Pointer Lock API' );
}
export { PointerLockControls };

4958
web/vendor/addons/loaders/GLTFLoader.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,361 @@
import {
Clock,
HalfFloatType,
NoBlending,
Vector2,
WebGLRenderTarget
} from 'three';
import { CopyShader } from '../shaders/CopyShader.js';
import { ShaderPass } from './ShaderPass.js';
import { ClearMaskPass, MaskPass } from './MaskPass.js';
/**
* Used to implement post-processing effects in three.js.
* The class manages a chain of post-processing passes to produce the final visual result.
* Post-processing passes are executed in order of their addition/insertion.
* The last pass is automatically rendered to screen.
*
* This module can only be used with {@link WebGLRenderer}.
*
* ```js
* const composer = new EffectComposer( renderer );
*
* // adding some passes
* const renderPass = new RenderPass( scene, camera );
* composer.addPass( renderPass );
*
* const glitchPass = new GlitchPass();
* composer.addPass( glitchPass );
*
* const outputPass = new OutputPass()
* composer.addPass( outputPass );
*
* function animate() {
*
* composer.render(); // instead of renderer.render()
*
* }
* ```
*/
class EffectComposer {
/**
* Constructs a new effect composer.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} [renderTarget] - This render target and a clone will
* be used as the internal read and write buffers. If not given, the composer creates
* the buffers automatically.
*/
constructor( renderer, renderTarget ) {
/**
* The renderer.
*
* @type {WebGLRenderer}
*/
this.renderer = renderer;
this._pixelRatio = renderer.getPixelRatio();
if ( renderTarget === undefined ) {
const size = renderer.getSize( new Vector2() );
this._width = size.width;
this._height = size.height;
renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } );
renderTarget.texture.name = 'EffectComposer.rt1';
} else {
this._width = renderTarget.width;
this._height = renderTarget.height;
}
this.renderTarget1 = renderTarget;
this.renderTarget2 = renderTarget.clone();
this.renderTarget2.texture.name = 'EffectComposer.rt2';
/**
* A reference to the internal write buffer. Passes usually write
* their result into this buffer.
*
* @type {WebGLRenderTarget}
*/
this.writeBuffer = this.renderTarget1;
/**
* A reference to the internal read buffer. Passes usually read
* the previous render result from this buffer.
*
* @type {WebGLRenderTarget}
*/
this.readBuffer = this.renderTarget2;
/**
* Whether the final pass is rendered to the screen (default framebuffer) or not.
*
* @type {boolean}
* @default true
*/
this.renderToScreen = true;
/**
* An array representing the (ordered) chain of post-processing passes.
*
* @type {Array<Pass>}
*/
this.passes = [];
/**
* A copy pass used for internal swap operations.
*
* @private
* @type {ShaderPass}
*/
this.copyPass = new ShaderPass( CopyShader );
this.copyPass.material.blending = NoBlending;
/**
* The intenral clock for managing time data.
*
* @private
* @type {Clock}
*/
this.clock = new Clock();
}
/**
* Swaps the internal read/write buffers.
*/
swapBuffers() {
const tmp = this.readBuffer;
this.readBuffer = this.writeBuffer;
this.writeBuffer = tmp;
}
/**
* Adds the given pass to the pass chain.
*
* @param {Pass} pass - The pass to add.
*/
addPass( pass ) {
this.passes.push( pass );
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
/**
* Inserts the given pass at a given index.
*
* @param {Pass} pass - The pass to insert.
* @param {number} index - The index into the pass chain.
*/
insertPass( pass, index ) {
this.passes.splice( index, 0, pass );
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
/**
* Removes the given pass from the pass chain.
*
* @param {Pass} pass - The pass to remove.
*/
removePass( pass ) {
const index = this.passes.indexOf( pass );
if ( index !== - 1 ) {
this.passes.splice( index, 1 );
}
}
/**
* Returns `true` if the pass for the given index is the last enabled pass in the pass chain.
*
* @param {number} passIndex - The pass index.
* @return {boolean} Whether the the pass for the given index is the last pass in the pass chain.
*/
isLastEnabledPass( passIndex ) {
for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {
if ( this.passes[ i ].enabled ) {
return false;
}
}
return true;
}
/**
* Executes all enabled post-processing passes in order to produce the final frame.
*
* @param {number} deltaTime - The delta time in seconds. If not given, the composer computes
* its own time delta value.
*/
render( deltaTime ) {
// deltaTime value is in seconds
if ( deltaTime === undefined ) {
deltaTime = this.clock.getDelta();
}
const currentRenderTarget = this.renderer.getRenderTarget();
let maskActive = false;
for ( let i = 0, il = this.passes.length; i < il; i ++ ) {
const pass = this.passes[ i ];
if ( pass.enabled === false ) continue;
pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
if ( pass.needsSwap ) {
if ( maskActive ) {
const context = this.renderer.getContext();
const stencil = this.renderer.state.buffers.stencil;
//context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );
stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );
this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );
//context.stencilFunc( context.EQUAL, 1, 0xffffffff );
stencil.setFunc( context.EQUAL, 1, 0xffffffff );
}
this.swapBuffers();
}
if ( MaskPass !== undefined ) {
if ( pass instanceof MaskPass ) {
maskActive = true;
} else if ( pass instanceof ClearMaskPass ) {
maskActive = false;
}
}
}
this.renderer.setRenderTarget( currentRenderTarget );
}
/**
* Resets the internal state of the EffectComposer.
*
* @param {WebGLRenderTarget} [renderTarget] - This render target has the same purpose like
* the one from the constructor. If set, it is used to setup the read and write buffers.
*/
reset( renderTarget ) {
if ( renderTarget === undefined ) {
const size = this.renderer.getSize( new Vector2() );
this._pixelRatio = this.renderer.getPixelRatio();
this._width = size.width;
this._height = size.height;
renderTarget = this.renderTarget1.clone();
renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
this.renderTarget1.dispose();
this.renderTarget2.dispose();
this.renderTarget1 = renderTarget;
this.renderTarget2 = renderTarget.clone();
this.writeBuffer = this.renderTarget1;
this.readBuffer = this.renderTarget2;
}
/**
* Resizes the internal read and write buffers as well as all passes. Similar to {@link WebGLRenderer#setSize},
* this method honors the current pixel ration.
*
* @param {number} width - The width in logical pixels.
* @param {number} height - The height in logical pixels.
*/
setSize( width, height ) {
this._width = width;
this._height = height;
const effectiveWidth = this._width * this._pixelRatio;
const effectiveHeight = this._height * this._pixelRatio;
this.renderTarget1.setSize( effectiveWidth, effectiveHeight );
this.renderTarget2.setSize( effectiveWidth, effectiveHeight );
for ( let i = 0; i < this.passes.length; i ++ ) {
this.passes[ i ].setSize( effectiveWidth, effectiveHeight );
}
}
/**
* Sets device pixel ratio. This is usually used for HiDPI device to prevent blurring output.
* Setting the pixel ratio will automatically resize the composer.
*
* @param {number} pixelRatio - The pixel ratio to set.
*/
setPixelRatio( pixelRatio ) {
this._pixelRatio = pixelRatio;
this.setSize( this._width, this._height );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the composer is no longer used in your app.
*/
dispose() {
this.renderTarget1.dispose();
this.renderTarget2.dispose();
this.copyPass.dispose();
}
}
export { EffectComposer };

View File

@ -0,0 +1,194 @@
import { Pass } from './Pass.js';
/**
* This pass can be used to define a mask during post processing.
* Meaning only areas of subsequent post processing are affected
* which lie in the masking area of this pass. Internally, the masking
* is implemented with the stencil buffer.
*
* ```js
* const maskPass = new MaskPass( scene, camera );
* composer.addPass( maskPass );
* ```
*
* @augments Pass
*/
class MaskPass extends Pass {
/**
* Constructs a new mask pass.
*
* @param {Scene} scene - The 3D objects in this scene will define the mask.
* @param {Camera} camera - The camera.
*/
constructor( scene, camera ) {
super();
/**
* The scene that defines the mask.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The camera.
*
* @type {Camera}
*/
this.camera = camera;
/**
* Overwritten to perform a clear operation by default.
*
* @type {boolean}
* @default true
*/
this.clear = true;
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
/**
* Whether to inverse the mask or not.
*
* @type {boolean}
* @default false
*/
this.inverse = false;
}
/**
* Performs a mask pass with the configured scene and camera.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
const context = renderer.getContext();
const state = renderer.state;
// don't update color or depth
state.buffers.color.setMask( false );
state.buffers.depth.setMask( false );
// lock buffers
state.buffers.color.setLocked( true );
state.buffers.depth.setLocked( true );
// set up stencil
let writeValue, clearValue;
if ( this.inverse ) {
writeValue = 0;
clearValue = 1;
} else {
writeValue = 1;
clearValue = 0;
}
state.buffers.stencil.setTest( true );
state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE );
state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff );
state.buffers.stencil.setClear( clearValue );
state.buffers.stencil.setLocked( true );
// draw into the stencil buffer
renderer.setRenderTarget( readBuffer );
if ( this.clear ) renderer.clear();
renderer.render( this.scene, this.camera );
renderer.setRenderTarget( writeBuffer );
if ( this.clear ) renderer.clear();
renderer.render( this.scene, this.camera );
// unlock color and depth buffer and make them writable for subsequent rendering/clearing
state.buffers.color.setLocked( false );
state.buffers.depth.setLocked( false );
state.buffers.color.setMask( true );
state.buffers.depth.setMask( true );
// only render where stencil is set to 1
state.buffers.stencil.setLocked( false );
state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1
state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP );
state.buffers.stencil.setLocked( true );
}
}
/**
* This pass can be used to clear a mask previously defined with {@link MaskPass}.
*
* ```js
* const clearPass = new ClearMaskPass();
* composer.addPass( clearPass );
* ```
*
* @augments Pass
*/
class ClearMaskPass extends Pass {
/**
* Constructs a new clear mask pass.
*/
constructor() {
super();
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
}
/**
* Performs the clear of the currently defined mask.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
renderer.state.buffers.stencil.setLocked( false );
renderer.state.buffers.stencil.setTest( false );
}
}
export { MaskPass, ClearMaskPass };

View File

@ -0,0 +1,138 @@
import {
ColorManagement,
RawShaderMaterial,
UniformsUtils,
LinearToneMapping,
ReinhardToneMapping,
CineonToneMapping,
AgXToneMapping,
ACESFilmicToneMapping,
NeutralToneMapping,
CustomToneMapping,
SRGBTransfer
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
import { OutputShader } from '../shaders/OutputShader.js';
/**
* This pass is responsible for including tone mapping and color space conversion
* into your pass chain. In most cases, this pass should be included at the end
* of each pass chain. If a pass requires sRGB input (e.g. like FXAA), the pass
* must follow `OutputPass` in the pass chain.
*
* The tone mapping and color space settings are extracted from the renderer.
*
* ```js
* const outputPass = new OutputPass();
* composer.addPass( outputPass );
* ```
*
* @augments Pass
*/
class OutputPass extends Pass {
/**
* Constructs a new output pass.
*/
constructor() {
super();
/**
* The pass uniforms.
*
* @type {Object}
*/
this.uniforms = UniformsUtils.clone( OutputShader.uniforms );
/**
* The pass material.
*
* @type {RawShaderMaterial}
*/
this.material = new RawShaderMaterial( {
name: OutputShader.name,
uniforms: this.uniforms,
vertexShader: OutputShader.vertexShader,
fragmentShader: OutputShader.fragmentShader
} );
// internals
this._fsQuad = new FullScreenQuad( this.material );
this._outputColorSpace = null;
this._toneMapping = null;
}
/**
* Performs the output pass.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer/*, deltaTime, maskActive */ ) {
this.uniforms[ 'tDiffuse' ].value = readBuffer.texture;
this.uniforms[ 'toneMappingExposure' ].value = renderer.toneMappingExposure;
// rebuild defines if required
if ( this._outputColorSpace !== renderer.outputColorSpace || this._toneMapping !== renderer.toneMapping ) {
this._outputColorSpace = renderer.outputColorSpace;
this._toneMapping = renderer.toneMapping;
this.material.defines = {};
if ( ColorManagement.getTransfer( this._outputColorSpace ) === SRGBTransfer ) this.material.defines.SRGB_TRANSFER = '';
if ( this._toneMapping === LinearToneMapping ) this.material.defines.LINEAR_TONE_MAPPING = '';
else if ( this._toneMapping === ReinhardToneMapping ) this.material.defines.REINHARD_TONE_MAPPING = '';
else if ( this._toneMapping === CineonToneMapping ) this.material.defines.CINEON_TONE_MAPPING = '';
else if ( this._toneMapping === ACESFilmicToneMapping ) this.material.defines.ACES_FILMIC_TONE_MAPPING = '';
else if ( this._toneMapping === AgXToneMapping ) this.material.defines.AGX_TONE_MAPPING = '';
else if ( this._toneMapping === NeutralToneMapping ) this.material.defines.NEUTRAL_TONE_MAPPING = '';
else if ( this._toneMapping === CustomToneMapping ) this.material.defines.CUSTOM_TONE_MAPPING = '';
this.material.needsUpdate = true;
}
//
if ( this.renderToScreen === true ) {
renderer.setRenderTarget( null );
this._fsQuad.render( renderer );
} else {
renderer.setRenderTarget( writeBuffer );
if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
this._fsQuad.render( renderer );
}
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*/
dispose() {
this.material.dispose();
this._fsQuad.dispose();
}
}
export { OutputPass };

189
web/vendor/addons/postprocessing/Pass.js vendored Normal file
View File

@ -0,0 +1,189 @@
import {
BufferGeometry,
Float32BufferAttribute,
OrthographicCamera,
Mesh
} from 'three';
/**
* Abstract base class for all post processing passes.
*
* This module is only relevant for post processing with {@link WebGLRenderer}.
*
* @abstract
*/
class Pass {
/**
* Constructs a new pass.
*/
constructor() {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isPass = true;
/**
* If set to `true`, the pass is processed by the composer.
*
* @type {boolean}
* @default true
*/
this.enabled = true;
/**
* If set to `true`, the pass indicates to swap read and write buffer after rendering.
*
* @type {boolean}
* @default true
*/
this.needsSwap = true;
/**
* If set to `true`, the pass clears its buffer before rendering
*
* @type {boolean}
* @default false
*/
this.clear = false;
/**
* If set to `true`, the result of the pass is rendered to screen. The last pass in the composers
* pass chain gets automatically rendered to screen, no matter how this property is configured.
*
* @type {boolean}
* @default false
*/
this.renderToScreen = false;
}
/**
* Sets the size of the pass.
*
* @abstract
* @param {number} width - The width to set.
* @param {number} height - The width to set.
*/
setSize( /* width, height */ ) {}
/**
* This method holds the render logic of a pass. It must be implemented in all derived classes.
*
* @abstract
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*
* @abstract
*/
dispose() {}
}
// Helper for passes that need to fill the viewport with a single quad.
const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
// https://github.com/mrdoob/three.js/pull/21358
class FullscreenTriangleGeometry extends BufferGeometry {
constructor() {
super();
this.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
this.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );
}
}
const _geometry = new FullscreenTriangleGeometry();
/**
* This module is a helper for passes which need to render a full
* screen effect which is quite common in context of post processing.
*
* The intended usage is to reuse a single full screen quad for rendering
* subsequent passes by just reassigning the `material` reference.
*
* This module can only be used with {@link WebGLRenderer}.
*
* @augments Mesh
*/
class FullScreenQuad {
/**
* Constructs a new full screen quad.
*
* @param {?Material} material - The material to render te full screen quad with.
*/
constructor( material ) {
this._mesh = new Mesh( _geometry, material );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the instance is no longer used in your app.
*/
dispose() {
this._mesh.geometry.dispose();
}
/**
* Renders the full screen quad.
*
* @param {WebGLRenderer} renderer - The renderer.
*/
render( renderer ) {
renderer.render( this._mesh, _camera );
}
/**
* The quad's material.
*
* @type {?Material}
*/
get material() {
return this._mesh.material;
}
set material( value ) {
this._mesh.material = value;
}
}
export { Pass, FullScreenQuad };

View File

@ -0,0 +1,182 @@
import {
Color
} from 'three';
import { Pass } from './Pass.js';
/**
* This class represents a render pass. It takes a camera and a scene and produces
* a beauty pass for subsequent post processing effects.
*
* ```js
* const renderPass = new RenderPass( scene, camera );
* composer.addPass( renderPass );
* ```
*
* @augments Pass
*/
class RenderPass extends Pass {
/**
* Constructs a new render pass.
*
* @param {Scene} scene - The scene to render.
* @param {Camera} camera - The camera.
* @param {?Material} [overrideMaterial=null] - The override material. If set, this material is used
* for all objects in the scene.
* @param {?(number|Color|string)} [clearColor=null] - The clear color of the render pass.
* @param {?number} [clearAlpha=null] - The clear alpha of the render pass.
*/
constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) {
super();
/**
* The scene to render.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The camera.
*
* @type {Camera}
*/
this.camera = camera;
/**
* The override material. If set, this material is used
* for all objects in the scene.
*
* @type {?Material}
* @default null
*/
this.overrideMaterial = overrideMaterial;
/**
* The clear color of the render pass.
*
* @type {?(number|Color|string)}
* @default null
*/
this.clearColor = clearColor;
/**
* The clear alpha of the render pass.
*
* @type {?number}
* @default null
*/
this.clearAlpha = clearAlpha;
/**
* Overwritten to perform a clear operation by default.
*
* @type {boolean}
* @default true
*/
this.clear = true;
/**
* If set to `true`, only the depth can be cleared when `clear` is to `false`.
*
* @type {boolean}
* @default false
*/
this.clearDepth = false;
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
this._oldClearColor = new Color();
}
/**
* Performs a beauty pass with the configured scene and camera.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
const oldAutoClear = renderer.autoClear;
renderer.autoClear = false;
let oldClearAlpha, oldOverrideMaterial;
if ( this.overrideMaterial !== null ) {
oldOverrideMaterial = this.scene.overrideMaterial;
this.scene.overrideMaterial = this.overrideMaterial;
}
if ( this.clearColor !== null ) {
renderer.getClearColor( this._oldClearColor );
renderer.setClearColor( this.clearColor, renderer.getClearAlpha() );
}
if ( this.clearAlpha !== null ) {
oldClearAlpha = renderer.getClearAlpha();
renderer.setClearAlpha( this.clearAlpha );
}
if ( this.clearDepth == true ) {
renderer.clearDepth();
}
renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );
if ( this.clear === true ) {
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
}
renderer.render( this.scene, this.camera );
// restore
if ( this.clearColor !== null ) {
renderer.setClearColor( this._oldClearColor );
}
if ( this.clearAlpha !== null ) {
renderer.setClearAlpha( oldClearAlpha );
}
if ( this.overrideMaterial !== null ) {
this.scene.overrideMaterial = oldOverrideMaterial;
}
renderer.autoClear = oldAutoClear;
}
}
export { RenderPass };

View File

@ -0,0 +1,134 @@
import {
ShaderMaterial,
UniformsUtils
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
/**
* This pass can be used to create a post processing effect
* with a raw GLSL shader object. Useful for implementing custom
* effects.
*
* ```js
* const fxaaPass = new ShaderPass( FXAAShader );
* composer.addPass( fxaaPass );
* ```
*
* @augments Pass
*/
class ShaderPass extends Pass {
/**
* Constructs a new shader pass.
*
* @param {Object|ShaderMaterial} [shader] - A shader object holding vertex and fragment shader as well as
* defines and uniforms. It's also valid to pass a custom shader material.
* @param {string} [textureID='tDiffuse'] - The name of the texture uniform that should sample
* the read buffer.
*/
constructor( shader, textureID = 'tDiffuse' ) {
super();
/**
* The name of the texture uniform that should sample the read buffer.
*
* @type {string}
* @default 'tDiffuse'
*/
this.textureID = textureID;
/**
* The pass uniforms.
*
* @type {?Object}
*/
this.uniforms = null;
/**
* The pass material.
*
* @type {?ShaderMaterial}
*/
this.material = null;
if ( shader instanceof ShaderMaterial ) {
this.uniforms = shader.uniforms;
this.material = shader;
} else if ( shader ) {
this.uniforms = UniformsUtils.clone( shader.uniforms );
this.material = new ShaderMaterial( {
name: ( shader.name !== undefined ) ? shader.name : 'unspecified',
defines: Object.assign( {}, shader.defines ),
uniforms: this.uniforms,
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader
} );
}
// internals
this._fsQuad = new FullScreenQuad( this.material );
}
/**
* Performs the shader pass.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
if ( this.uniforms[ this.textureID ] ) {
this.uniforms[ this.textureID ].value = readBuffer.texture;
}
this._fsQuad.material = this.material;
if ( this.renderToScreen ) {
renderer.setRenderTarget( null );
this._fsQuad.render( renderer );
} else {
renderer.setRenderTarget( writeBuffer );
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
this._fsQuad.render( renderer );
}
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*/
dispose() {
this.material.dispose();
this._fsQuad.dispose();
}
}
export { ShaderPass };

View File

@ -0,0 +1,491 @@
import {
AdditiveBlending,
Color,
HalfFloatType,
MeshBasicMaterial,
ShaderMaterial,
UniformsUtils,
Vector2,
Vector3,
WebGLRenderTarget
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
import { CopyShader } from '../shaders/CopyShader.js';
import { LuminosityHighPassShader } from '../shaders/LuminosityHighPassShader.js';
/**
* This pass is inspired by the bloom pass of Unreal Engine. It creates a
* mip map chain of bloom textures and blurs them with different radii. Because
* of the weighted combination of mips, and because larger blurs are done on
* higher mips, this effect provides good quality and performance.
*
* When using this pass, tone mapping must be enabled in the renderer settings.
*
* Reference:
* - [Bloom in Unreal Engine]{@link https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/}
*
* ```js
* const resolution = new THREE.Vector2( window.innerWidth, window.innerHeight );
* const bloomPass = new UnrealBloomPass( resolution, 1.5, 0.4, 0.85 );
* composer.addPass( bloomPass );
* ```
*
* @augments Pass
*/
class UnrealBloomPass extends Pass {
/**
* Constructs a new Unreal Bloom pass.
*
* @param {Vector2} [resolution] - The effect's resolution.
* @param {number} [strength=1] - The Bloom strength.
* @param {number} radius - The Bloom radius.
* @param {number} threshold - The luminance threshold limits which bright areas contribute to the Bloom effect.
*/
constructor( resolution, strength = 1, radius, threshold ) {
super();
/**
* The Bloom strength.
*
* @type {number}
* @default 1
*/
this.strength = strength;
/**
* The Bloom radius.
*
* @type {number}
*/
this.radius = radius;
/**
* The luminance threshold limits which bright areas contribute to the Bloom effect.
*
* @type {number}
*/
this.threshold = threshold;
/**
* The effect's resolution.
*
* @type {Vector2}
* @default (256,256)
*/
this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );
/**
* The effect's clear color
*
* @type {Color}
* @default (0,0,0)
*/
this.clearColor = new Color( 0, 0, 0 );
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
// internals
// render targets
this.renderTargetsHorizontal = [];
this.renderTargetsVertical = [];
this.nMips = 5;
let resx = Math.round( this.resolution.x / 2 );
let resy = Math.round( this.resolution.y / 2 );
this.renderTargetBright = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
this.renderTargetBright.texture.name = 'UnrealBloomPass.bright';
this.renderTargetBright.texture.generateMipmaps = false;
for ( let i = 0; i < this.nMips; i ++ ) {
const renderTargetHorizontal = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
renderTargetHorizontal.texture.name = 'UnrealBloomPass.h' + i;
renderTargetHorizontal.texture.generateMipmaps = false;
this.renderTargetsHorizontal.push( renderTargetHorizontal );
const renderTargetVertical = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i;
renderTargetVertical.texture.generateMipmaps = false;
this.renderTargetsVertical.push( renderTargetVertical );
resx = Math.round( resx / 2 );
resy = Math.round( resy / 2 );
}
// luminosity high pass material
const highPassShader = LuminosityHighPassShader;
this.highPassUniforms = UniformsUtils.clone( highPassShader.uniforms );
this.highPassUniforms[ 'luminosityThreshold' ].value = threshold;
this.highPassUniforms[ 'smoothWidth' ].value = 0.01;
this.materialHighPassFilter = new ShaderMaterial( {
uniforms: this.highPassUniforms,
vertexShader: highPassShader.vertexShader,
fragmentShader: highPassShader.fragmentShader
} );
// gaussian blur materials
this.separableBlurMaterials = [];
const kernelSizeArray = [ 3, 5, 7, 9, 11 ];
resx = Math.round( this.resolution.x / 2 );
resy = Math.round( this.resolution.y / 2 );
for ( let i = 0; i < this.nMips; i ++ ) {
this.separableBlurMaterials.push( this._getSeparableBlurMaterial( kernelSizeArray[ i ] ) );
this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );
resx = Math.round( resx / 2 );
resy = Math.round( resy / 2 );
}
// composite material
this.compositeMaterial = this._getCompositeMaterial( this.nMips );
this.compositeMaterial.uniforms[ 'blurTexture1' ].value = this.renderTargetsVertical[ 0 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture2' ].value = this.renderTargetsVertical[ 1 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture3' ].value = this.renderTargetsVertical[ 2 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture4' ].value = this.renderTargetsVertical[ 3 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture5' ].value = this.renderTargetsVertical[ 4 ].texture;
this.compositeMaterial.uniforms[ 'bloomStrength' ].value = strength;
this.compositeMaterial.uniforms[ 'bloomRadius' ].value = 0.1;
const bloomFactors = [ 1.0, 0.8, 0.6, 0.4, 0.2 ];
this.compositeMaterial.uniforms[ 'bloomFactors' ].value = bloomFactors;
this.bloomTintColors = [ new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ) ];
this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
// blend material
this.copyUniforms = UniformsUtils.clone( CopyShader.uniforms );
this.blendMaterial = new ShaderMaterial( {
uniforms: this.copyUniforms,
vertexShader: CopyShader.vertexShader,
fragmentShader: CopyShader.fragmentShader,
blending: AdditiveBlending,
depthTest: false,
depthWrite: false,
transparent: true
} );
this._oldClearColor = new Color();
this._oldClearAlpha = 1;
this._basic = new MeshBasicMaterial();
this._fsQuad = new FullScreenQuad( null );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*/
dispose() {
for ( let i = 0; i < this.renderTargetsHorizontal.length; i ++ ) {
this.renderTargetsHorizontal[ i ].dispose();
}
for ( let i = 0; i < this.renderTargetsVertical.length; i ++ ) {
this.renderTargetsVertical[ i ].dispose();
}
this.renderTargetBright.dispose();
//
for ( let i = 0; i < this.separableBlurMaterials.length; i ++ ) {
this.separableBlurMaterials[ i ].dispose();
}
this.compositeMaterial.dispose();
this.blendMaterial.dispose();
this._basic.dispose();
//
this._fsQuad.dispose();
}
/**
* Sets the size of the pass.
*
* @param {number} width - The width to set.
* @param {number} height - The width to set.
*/
setSize( width, height ) {
let resx = Math.round( width / 2 );
let resy = Math.round( height / 2 );
this.renderTargetBright.setSize( resx, resy );
for ( let i = 0; i < this.nMips; i ++ ) {
this.renderTargetsHorizontal[ i ].setSize( resx, resy );
this.renderTargetsVertical[ i ].setSize( resx, resy );
this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );
resx = Math.round( resx / 2 );
resy = Math.round( resy / 2 );
}
}
/**
* Performs the Bloom pass.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
renderer.getClearColor( this._oldClearColor );
this._oldClearAlpha = renderer.getClearAlpha();
const oldAutoClear = renderer.autoClear;
renderer.autoClear = false;
renderer.setClearColor( this.clearColor, 0 );
if ( maskActive ) renderer.state.buffers.stencil.setTest( false );
// Render input to screen
if ( this.renderToScreen ) {
this._fsQuad.material = this._basic;
this._basic.map = readBuffer.texture;
renderer.setRenderTarget( null );
renderer.clear();
this._fsQuad.render( renderer );
}
// 1. Extract Bright Areas
this.highPassUniforms[ 'tDiffuse' ].value = readBuffer.texture;
this.highPassUniforms[ 'luminosityThreshold' ].value = this.threshold;
this._fsQuad.material = this.materialHighPassFilter;
renderer.setRenderTarget( this.renderTargetBright );
renderer.clear();
this._fsQuad.render( renderer );
// 2. Blur All the mips progressively
let inputRenderTarget = this.renderTargetBright;
for ( let i = 0; i < this.nMips; i ++ ) {
this._fsQuad.material = this.separableBlurMaterials[ i ];
this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = inputRenderTarget.texture;
this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionX;
renderer.setRenderTarget( this.renderTargetsHorizontal[ i ] );
renderer.clear();
this._fsQuad.render( renderer );
this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = this.renderTargetsHorizontal[ i ].texture;
this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionY;
renderer.setRenderTarget( this.renderTargetsVertical[ i ] );
renderer.clear();
this._fsQuad.render( renderer );
inputRenderTarget = this.renderTargetsVertical[ i ];
}
// Composite All the mips
this._fsQuad.material = this.compositeMaterial;
this.compositeMaterial.uniforms[ 'bloomStrength' ].value = this.strength;
this.compositeMaterial.uniforms[ 'bloomRadius' ].value = this.radius;
this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
renderer.setRenderTarget( this.renderTargetsHorizontal[ 0 ] );
renderer.clear();
this._fsQuad.render( renderer );
// Blend it additively over the input texture
this._fsQuad.material = this.blendMaterial;
this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetsHorizontal[ 0 ].texture;
if ( maskActive ) renderer.state.buffers.stencil.setTest( true );
if ( this.renderToScreen ) {
renderer.setRenderTarget( null );
this._fsQuad.render( renderer );
} else {
renderer.setRenderTarget( readBuffer );
this._fsQuad.render( renderer );
}
// Restore renderer settings
renderer.setClearColor( this._oldClearColor, this._oldClearAlpha );
renderer.autoClear = oldAutoClear;
}
// internals
_getSeparableBlurMaterial( kernelRadius ) {
const coefficients = [];
for ( let i = 0; i < kernelRadius; i ++ ) {
coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( kernelRadius * kernelRadius ) ) / kernelRadius );
}
return new ShaderMaterial( {
defines: {
'KERNEL_RADIUS': kernelRadius
},
uniforms: {
'colorTexture': { value: null },
'invSize': { value: new Vector2( 0.5, 0.5 ) }, // inverse texture size
'direction': { value: new Vector2( 0.5, 0.5 ) },
'gaussianCoefficients': { value: coefficients } // precomputed Gaussian coefficients
},
vertexShader:
`varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader:
`#include <common>
varying vec2 vUv;
uniform sampler2D colorTexture;
uniform vec2 invSize;
uniform vec2 direction;
uniform float gaussianCoefficients[KERNEL_RADIUS];
void main() {
float weightSum = gaussianCoefficients[0];
vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
float x = float(i);
float w = gaussianCoefficients[i];
vec2 uvOffset = direction * invSize * x;
vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb;
vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb;
diffuseSum += (sample1 + sample2) * w;
weightSum += 2.0 * w;
}
gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
}`
} );
}
_getCompositeMaterial( nMips ) {
return new ShaderMaterial( {
defines: {
'NUM_MIPS': nMips
},
uniforms: {
'blurTexture1': { value: null },
'blurTexture2': { value: null },
'blurTexture3': { value: null },
'blurTexture4': { value: null },
'blurTexture5': { value: null },
'bloomStrength': { value: 1.0 },
'bloomFactors': { value: null },
'bloomTintColors': { value: null },
'bloomRadius': { value: 0.0 }
},
vertexShader:
`varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader:
`varying vec2 vUv;
uniform sampler2D blurTexture1;
uniform sampler2D blurTexture2;
uniform sampler2D blurTexture3;
uniform sampler2D blurTexture4;
uniform sampler2D blurTexture5;
uniform float bloomStrength;
uniform float bloomRadius;
uniform float bloomFactors[NUM_MIPS];
uniform vec3 bloomTintColors[NUM_MIPS];
float lerpBloomFactor(const in float factor) {
float mirrorFactor = 1.2 - factor;
return mix(factor, mirrorFactor, bloomRadius);
}
void main() {
gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) +
lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) +
lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) +
lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) +
lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) );
}`
} );
}
}
UnrealBloomPass.BlurDirectionX = new Vector2( 1.0, 0.0 );
UnrealBloomPass.BlurDirectionY = new Vector2( 0.0, 1.0 );
export { UnrealBloomPass };

49
web/vendor/addons/shaders/CopyShader.js vendored Normal file
View File

@ -0,0 +1,49 @@
/** @module CopyShader */
/**
* Full-screen copy shader pass.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const CopyShader = {
name: 'CopyShader',
uniforms: {
'tDiffuse': { value: null },
'opacity': { value: 1.0 }
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
uniform float opacity;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = opacity * texel;
}`
};
export { CopyShader };

View File

@ -0,0 +1,65 @@
import {
Color
} from 'three';
/** @module LuminosityHighPassShader */
/**
* Luminosity high pass shader.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const LuminosityHighPassShader = {
name: 'LuminosityHighPassShader',
uniforms: {
'tDiffuse': { value: null },
'luminosityThreshold': { value: 1.0 },
'smoothWidth': { value: 1.0 },
'defaultColor': { value: new Color( 0x000000 ) },
'defaultOpacity': { value: 0.0 }
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
uniform sampler2D tDiffuse;
uniform vec3 defaultColor;
uniform float defaultOpacity;
uniform float luminosityThreshold;
uniform float smoothWidth;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
float v = luminance( texel.xyz );
vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity );
float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );
gl_FragColor = mix( outputColor, texel, alpha );
}`
};
export { LuminosityHighPassShader };

View File

@ -0,0 +1,100 @@
/** @module OutputShader */
/**
* Performs tone mapping and color space conversion for
* FX workflows.
*
* Used by {@link OutputPass}.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const OutputShader = {
name: 'OutputShader',
uniforms: {
'tDiffuse': { value: null },
'toneMappingExposure': { value: 1 }
},
vertexShader: /* glsl */`
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
precision highp float;
uniform sampler2D tDiffuse;
#include <tonemapping_pars_fragment>
#include <colorspace_pars_fragment>
varying vec2 vUv;
void main() {
gl_FragColor = texture2D( tDiffuse, vUv );
// tone mapping
#ifdef LINEAR_TONE_MAPPING
gl_FragColor.rgb = LinearToneMapping( gl_FragColor.rgb );
#elif defined( REINHARD_TONE_MAPPING )
gl_FragColor.rgb = ReinhardToneMapping( gl_FragColor.rgb );
#elif defined( CINEON_TONE_MAPPING )
gl_FragColor.rgb = CineonToneMapping( gl_FragColor.rgb );
#elif defined( ACES_FILMIC_TONE_MAPPING )
gl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb );
#elif defined( AGX_TONE_MAPPING )
gl_FragColor.rgb = AgXToneMapping( gl_FragColor.rgb );
#elif defined( NEUTRAL_TONE_MAPPING )
gl_FragColor.rgb = NeutralToneMapping( gl_FragColor.rgb );
#elif defined( CUSTOM_TONE_MAPPING )
gl_FragColor.rgb = CustomToneMapping( gl_FragColor.rgb );
#endif
// color space
#ifdef SRGB_TRANSFER
gl_FragColor = sRGBTransferOETF( gl_FragColor );
#endif
}`
};
export { OutputShader };

View File

@ -0,0 +1,53 @@
/** @module VignetteShader */
/**
* Based on [PaintEffect postprocess from ro.me]{@link http://code.google.com/p/3-dreams-of-black/source/browse/deploy/js/effects/PaintEffect.js}.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const VignetteShader = {
name: 'VignetteShader',
uniforms: {
'tDiffuse': { value: null },
'offset': { value: 1.0 },
'darkness': { value: 1.0 }
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
uniform float offset;
uniform float darkness;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
// Eskil's vignette
vec4 texel = texture2D( tDiffuse, vUv );
vec2 uv = ( vUv - vec2( 0.5 ) ) * vec2( offset );
gl_FragColor = vec4( mix( texel.rgb, vec3( 1.0 - darkness ), dot( uv, uv ) ), texel.a );
}`
};
export { VignetteShader };

File diff suppressed because it is too large Load Diff

490
web/vendor/addons/utils/SkeletonUtils.js vendored Normal file
View File

@ -0,0 +1,490 @@
import {
AnimationClip,
AnimationMixer,
Matrix4,
Quaternion,
QuaternionKeyframeTrack,
SkeletonHelper,
Vector3,
VectorKeyframeTrack
} from 'three';
/**
* @module SkeletonUtils
* @three_import import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
*/
function getBoneName( bone, options ) {
if ( options.getBoneName !== undefined ) {
return options.getBoneName( bone );
}
return options.names[ bone.name ];
}
/**
* Retargets the skeleton from the given source 3D object to the
* target 3D object.
*
* @param {Object3D} target - The target 3D object.
* @param {Object3D} source - The source 3D object.
* @param {module:SkeletonUtils~RetargetOptions} options - The options.
*/
function retarget( target, source, options = {} ) {
const quat = new Quaternion(),
scale = new Vector3(),
relativeMatrix = new Matrix4(),
globalMatrix = new Matrix4();
options.preserveBoneMatrix = options.preserveBoneMatrix !== undefined ? options.preserveBoneMatrix : true;
options.preserveBonePositions = options.preserveBonePositions !== undefined ? options.preserveBonePositions : true;
options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false;
options.hip = options.hip !== undefined ? options.hip : 'hip';
options.hipInfluence = options.hipInfluence !== undefined ? options.hipInfluence : new Vector3( 1, 1, 1 );
options.scale = options.scale !== undefined ? options.scale : 1;
options.names = options.names || {};
const sourceBones = source.isObject3D ? source.skeleton.bones : getBones( source ),
bones = target.isObject3D ? target.skeleton.bones : getBones( target );
let bone, name, boneTo,
bonesPosition;
// reset bones
if ( target.isObject3D ) {
target.skeleton.pose();
} else {
options.useTargetMatrix = true;
options.preserveBoneMatrix = false;
}
if ( options.preserveBonePositions ) {
bonesPosition = [];
for ( let i = 0; i < bones.length; i ++ ) {
bonesPosition.push( bones[ i ].position.clone() );
}
}
if ( options.preserveBoneMatrix ) {
// reset matrix
target.updateMatrixWorld();
target.matrixWorld.identity();
// reset children matrix
for ( let i = 0; i < target.children.length; ++ i ) {
target.children[ i ].updateMatrixWorld( true );
}
}
for ( let i = 0; i < bones.length; ++ i ) {
bone = bones[ i ];
name = getBoneName( bone, options );
boneTo = getBoneByName( name, sourceBones );
globalMatrix.copy( bone.matrixWorld );
if ( boneTo ) {
boneTo.updateMatrixWorld();
if ( options.useTargetMatrix ) {
relativeMatrix.copy( boneTo.matrixWorld );
} else {
relativeMatrix.copy( target.matrixWorld ).invert();
relativeMatrix.multiply( boneTo.matrixWorld );
}
// ignore scale to extract rotation
scale.setFromMatrixScale( relativeMatrix );
relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) );
// apply to global matrix
globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) );
if ( target.isObject3D ) {
if ( options.localOffsets ) {
if ( options.localOffsets[ bone.name ] ) {
globalMatrix.multiply( options.localOffsets[ bone.name ] );
}
}
}
globalMatrix.copyPosition( relativeMatrix );
}
if ( name === options.hip ) {
globalMatrix.elements[ 12 ] *= options.scale * options.hipInfluence.x;
globalMatrix.elements[ 13 ] *= options.scale * options.hipInfluence.y;
globalMatrix.elements[ 14 ] *= options.scale * options.hipInfluence.z;
if ( options.hipPosition !== undefined ) {
globalMatrix.elements[ 12 ] += options.hipPosition.x * options.scale;
globalMatrix.elements[ 13 ] += options.hipPosition.y * options.scale;
globalMatrix.elements[ 14 ] += options.hipPosition.z * options.scale;
}
}
if ( bone.parent ) {
bone.matrix.copy( bone.parent.matrixWorld ).invert();
bone.matrix.multiply( globalMatrix );
} else {
bone.matrix.copy( globalMatrix );
}
bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
bone.updateMatrixWorld();
}
if ( options.preserveBonePositions ) {
for ( let i = 0; i < bones.length; ++ i ) {
bone = bones[ i ];
name = getBoneName( bone, options ) || bone.name;
if ( name !== options.hip ) {
bone.position.copy( bonesPosition[ i ] );
}
}
}
if ( options.preserveBoneMatrix ) {
// restore matrix
target.updateMatrixWorld( true );
}
}
/**
* Retargets the animation clip of the source object to the
* target 3D object.
*
* @param {Object3D} target - The target 3D object.
* @param {Object3D} source - The source 3D object.
* @param {AnimationClip} clip - The animation clip.
* @param {module:SkeletonUtils~RetargetOptions} options - The options.
* @return {AnimationClip} The retargeted animation clip.
*/
function retargetClip( target, source, clip, options = {} ) {
options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
// Calculate the fps from the source clip based on the track with the most frames, unless fps is already provided.
options.fps = options.fps !== undefined ? options.fps : ( Math.max( ...clip.tracks.map( track => track.times.length ) ) / clip.duration );
options.names = options.names || [];
if ( ! source.isObject3D ) {
source = getHelperFromSkeleton( source );
}
const numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ),
delta = clip.duration / ( numFrames - 1 ),
convertedTracks = [],
mixer = new AnimationMixer( source ),
bones = getBones( target.skeleton ),
boneDatas = [];
let positionOffset,
bone, boneTo, boneData,
name;
mixer.clipAction( clip ).play();
// trim
let start = 0, end = numFrames;
if ( options.trim !== undefined ) {
start = Math.round( options.trim[ 0 ] * options.fps );
end = Math.min( Math.round( options.trim[ 1 ] * options.fps ), numFrames ) - start;
mixer.update( options.trim[ 0 ] );
} else {
mixer.update( 0 );
}
source.updateMatrixWorld();
//
for ( let frame = 0; frame < end; ++ frame ) {
const time = frame * delta;
retarget( target, source, options );
for ( let j = 0; j < bones.length; ++ j ) {
bone = bones[ j ];
name = getBoneName( bone, options ) || bone.name;
boneTo = getBoneByName( name, source.skeleton );
if ( boneTo ) {
boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone };
if ( options.hip === name ) {
if ( ! boneData.pos ) {
boneData.pos = {
times: new Float32Array( end ),
values: new Float32Array( end * 3 )
};
}
if ( options.useFirstFramePosition ) {
if ( frame === 0 ) {
positionOffset = bone.position.clone();
}
bone.position.sub( positionOffset );
}
boneData.pos.times[ frame ] = time;
bone.position.toArray( boneData.pos.values, frame * 3 );
}
if ( ! boneData.quat ) {
boneData.quat = {
times: new Float32Array( end ),
values: new Float32Array( end * 4 )
};
}
boneData.quat.times[ frame ] = time;
bone.quaternion.toArray( boneData.quat.values, frame * 4 );
}
}
if ( frame === end - 2 ) {
// last mixer update before final loop iteration
// make sure we do not go over or equal to clip duration
mixer.update( delta - 0.0000001 );
} else {
mixer.update( delta );
}
source.updateMatrixWorld();
}
for ( let i = 0; i < boneDatas.length; ++ i ) {
boneData = boneDatas[ i ];
if ( boneData ) {
if ( boneData.pos ) {
convertedTracks.push( new VectorKeyframeTrack(
'.bones[' + boneData.bone.name + '].position',
boneData.pos.times,
boneData.pos.values
) );
}
convertedTracks.push( new QuaternionKeyframeTrack(
'.bones[' + boneData.bone.name + '].quaternion',
boneData.quat.times,
boneData.quat.values
) );
}
}
mixer.uncacheAction( clip );
return new AnimationClip( clip.name, - 1, convertedTracks );
}
/**
* Clones the given 3D object and its descendants, ensuring that any `SkinnedMesh` instances are
* correctly associated with their bones. Bones are also cloned, and must be descendants of the
* object passed to this method. Other data, like geometries and materials, are reused by reference.
*
* @param {Object3D} source - The 3D object to clone.
* @return {Object3D} The cloned 3D object.
*/
function clone( source ) {
const sourceLookup = new Map();
const cloneLookup = new Map();
const clone = source.clone();
parallelTraverse( source, clone, function ( sourceNode, clonedNode ) {
sourceLookup.set( clonedNode, sourceNode );
cloneLookup.set( sourceNode, clonedNode );
} );
clone.traverse( function ( node ) {
if ( ! node.isSkinnedMesh ) return;
const clonedMesh = node;
const sourceMesh = sourceLookup.get( node );
const sourceBones = sourceMesh.skeleton.bones;
clonedMesh.skeleton = sourceMesh.skeleton.clone();
clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix );
clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) {
return cloneLookup.get( bone );
} );
clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix );
} );
return clone;
}
// internal helper
function getBoneByName( name, skeleton ) {
for ( let i = 0, bones = getBones( skeleton ); i < bones.length; i ++ ) {
if ( name === bones[ i ].name )
return bones[ i ];
}
}
function getBones( skeleton ) {
return Array.isArray( skeleton ) ? skeleton : skeleton.bones;
}
function getHelperFromSkeleton( skeleton ) {
const source = new SkeletonHelper( skeleton.bones[ 0 ] );
source.skeleton = skeleton;
return source;
}
function parallelTraverse( a, b, callback ) {
callback( a, b );
for ( let i = 0; i < a.children.length; i ++ ) {
parallelTraverse( a.children[ i ], b.children[ i ], callback );
}
}
/**
* Retarget options of `SkeletonUtils`.
*
* @typedef {Object} module:SkeletonUtils~RetargetOptions
* @property {boolean} [useFirstFramePosition=false] - Whether to use the position of the first frame or not.
* @property {number} [fps] - The FPS of the clip.
* @property {Object<string,string>} [names] - A dictionary for mapping target to source bone names.
* @property {function(string):string} [getBoneName] - A function for mapping bone names. Alternative to `names`.
* @property {Array<number>} [trim] - Whether to trim the clip or not. If set the array should hold two values for the start and end.
* @property {boolean} [preserveBoneMatrix=true] - Whether to preserve bone matrices or not.
* @property {boolean} [preserveBonePositions=true] - Whether to preserve bone positions or not.
* @property {boolean} [useTargetMatrix=false] - Whether to use the target matrix or not.
* @property {string} [hip='hip'] - The name of the source's hip bone.
* @property {Vector3} [hipInfluence=(1,1,1)] - The hip influence.
* @property {number} [scale=1] - The scale.
**/
export {
retarget,
retargetClip,
clone,
};

57362
web/vendor/three.core.js vendored Normal file

File diff suppressed because one or more lines are too long

17990
web/vendor/three.module.js vendored Normal file

File diff suppressed because one or more lines are too long