├── package.json ├── src ├── notes ├── lib │ ├── useDarkScene.js │ ├── usePostEffects.js │ ├── material │ │ └── BoxBorderMaterial_v2.js │ ├── misc │ │ ├── VoxelGrid.js │ │ └── VoxelGrid_v1.js │ ├── meshes │ │ ├── Cube.js │ │ ├── DynLineMesh.js │ │ └── ShapePointsMesh.js │ └── useThreeWebGL2.js └── App.js ├── import-map.js ├── thirdparty ├── threePostProcess │ ├── shaders │ │ ├── CopyShader.js │ │ ├── LuminosityHighPassShader.js │ │ └── OutputShader.js │ ├── ShaderPass.js │ ├── Pass.js │ ├── RenderPass.js │ ├── OutputPass.js │ ├── MaskPass.js │ ├── EffectComposer.js │ └── UnrealBloomPass.js ├── notes.txt ├── OrbitControls.js └── BufferGeometryUtils.js ├── prototypes ├── _notes.txt ├── 002_voxel_grid.html ├── 000_pieces.html ├── 001_grid.html └── 003_rotate.html ├── LICENSE ├── README.md ├── index.html ├── .gitignore └── bs-config.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "tetris", 3 | "version" : "0.0.0.1", 4 | "description" : "Tetris game", 5 | "keywords" : [ "webgl", "threejs" ], 6 | "repository" : { "url": "https://github.com/sketchpunklabs/tetris.git", "type": "git" }, 7 | "author" : { "name": "Sketchpunk", "email": "tmp@tmp.com", "url": "http://tmp" }, 8 | "license" : "MIT", 9 | 10 | "devDependencies": { 11 | "browser-sync" : "^2.27.5" 12 | }, 13 | "scripts": { 14 | "dev" : "browser-sync start --config bs-config.js" 15 | } 16 | } -------------------------------------------------------------------------------- /src/notes: -------------------------------------------------------------------------------- 1 | https://tetris.fandom.com/wiki/Random_Generator 2 | 3 | https://github.com/RylanBot/threejs-tetris-react 4 | 5 | https://www.freecodecamp.org/news/learn-javascript-by-creating-a-tetris-game 6 | 7 | https://twitter.com/SquareAnon/status/1169006963868524551 8 | https://squaredev.itch.io/4-block-dungeon-prototype 9 | 10 | 11 | https://tetris.wiki/TGM_randomizer 12 | https://tetrisconcept.net/threads/randomizer-theory.512/page-12#post-65418 13 | 14 | Tetris99 RNG 7 Piece Bag 15 | https://www.youtube.com/watch?v=uX5btMAge5M -------------------------------------------------------------------------------- /import-map.js: -------------------------------------------------------------------------------- 1 | // in the future can prob do : 2 | const prepend = ( document.location.hostname.indexOf( 'localhost' ) === -1 )? '/kaykit_halloween' : ''; 3 | 4 | document.body.appendChild(Object.assign(document.createElement('script'), { 5 | type : 'importmap', 6 | innerHTML : ` 7 | {"imports":{ 8 | "three" : "${prepend}/thirdparty/three.module.js", 9 | "OrbitControls" : "${prepend}/thirdparty/OrbitControls.js", 10 | "TransformControls" : "${prepend}/thirdparty/TransformControls.js", 11 | "gl-matrix" : "${prepend}/thirdparty/gl-matrix/index.js", 12 | "postprocess/" : "${prepend}/thirdparty/threePostProcess/", 13 | "tp/" : "${prepend}/thirdparty/" 14 | }} 15 | `})); -------------------------------------------------------------------------------- /thirdparty/threePostProcess/shaders/CopyShader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Full-screen textured quad shader 3 | */ 4 | 5 | const CopyShader = { 6 | 7 | name: 'CopyShader', 8 | 9 | uniforms: { 10 | 11 | 'tDiffuse': { value: null }, 12 | 'opacity': { value: 1.0 } 13 | 14 | }, 15 | 16 | vertexShader: /* glsl */` 17 | 18 | varying vec2 vUv; 19 | 20 | void main() { 21 | 22 | vUv = uv; 23 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 24 | 25 | }`, 26 | 27 | fragmentShader: /* glsl */` 28 | 29 | uniform float opacity; 30 | 31 | uniform sampler2D tDiffuse; 32 | 33 | varying vec2 vUv; 34 | 35 | void main() { 36 | 37 | vec4 texel = texture2D( tDiffuse, vUv ); 38 | gl_FragColor = opacity * texel; 39 | 40 | 41 | }` 42 | 43 | }; 44 | 45 | export { CopyShader }; 46 | -------------------------------------------------------------------------------- /src/lib/useDarkScene.js: -------------------------------------------------------------------------------- 1 | import { DirectionalLight, AmbientLight, GridHelper } from 'three'; 2 | 3 | export default function useDarkScene( tjs ){ 4 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | // Light 6 | const light = new DirectionalLight( 0xffffff, 0.8 ); 7 | light.position.set( 5, 10, -5 ); 8 | tjs.scene.add( light ); 9 | 10 | tjs.scene.add( new AmbientLight( 0xa0a0a0 ) ); 11 | 12 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 | // Floor 14 | const gridSize = 10; 15 | const cellSize = 4; 16 | tjs.scene.add( new GridHelper( gridSize*cellSize, gridSize, 0x4f4f4f, 0x404040 ) ); 17 | 18 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 19 | // Renderer 20 | // tjs.renderer.setClearColor( 0x3a3a3a, 1 ); 21 | return tjs; 22 | }; -------------------------------------------------------------------------------- /prototypes/_notes.txt: -------------------------------------------------------------------------------- 1 | https://github.com/RylanBot/threejs-tetris-react 2 | 3 | 4 | - Get Next Random Piece 5 | - Each tick 6 | -- Move mino down 7 | -- Test if can not longer move down 8 | ---- If done, Check for tetris 9 | ------- If so, remove all possible rows 10 | ------- Update counter for successful ticks 11 | ------- More Rows gives you better score 12 | ------- Decrease tick duration 13 | 14 | Press down to end tick early 15 | 16 | On Rotate or Movement 17 | -- Test if new location is possible 18 | -- Show final placement possibility 19 | 20 | Use Instancing? 21 | -- At the end of every tick, Regenerate 22 | instance buffers with the results of the 23 | voxel grid. 24 | 25 | 26 | Game 27 | .tickDuration = 5 28 | .ticks = 0; 29 | .score = 0; 30 | .isPaused = false; 31 | 32 | .grid = new VoxelGrid(); 33 | .floor = new BoxGrid(); 34 | 35 | .bag = new Random7(); 36 | .next() -- Get Next Piece 37 | .preview() -- Preview next Piece 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sketchpunk Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tetris 2 | 3 | [![twitter](https://img.shields.io/badge/Twitter-profile-blue?style=flat-square&logo=twitter)](https://twitter.com/SketchpunkLabs) 4 | [![mastodon](https://img.shields.io/badge/Mastodon-profile-blue?style=flat-square&logo=mastodon)](https://mastodon.gamedev.place/@sketchpunk) 5 | [![bluesky](https://img.shields.io/badge/Bluesky-profile-blue?style=flat-square&logo=threads)](https://bsky.app/profile/sketchpunk.bsky.social) 6 | [![bluesky](https://img.shields.io/badge/Threads-profile-blue?style=flat-square&logo=threads)](https://www.threads.net/@sketchpunklabs) 7 | 8 | 9 | [![youtube](https://img.shields.io/badge/Youtube-subscribe-red?style=flat-square&logo=youtube)](https://youtube.com/c/sketchpunklabs) 10 | [![github](https://img.shields.io/badge/Sponsor-donate-red?style=flat-square&logo=github)](https://github.com/sponsors/sketchpunklabs) 11 | [![Patreon](https://img.shields.io/badge/Patreon-donate-red?style=flat-square&logo=youtube)](https://www.patreon.com/sketchpunk) 12 | 13 |
14 | Live Demo: https://sketchpunklabs.github.io/tetris/ 15 | 16 | ### TL;DR ### 17 | Lorem 18 | 19 | ### Development Setup ### 20 | ``` 21 | git clone --depth=1 https://github.com/sketchpunklabs/tetris 22 | cd tetris 23 | npm install 24 | npm run dev 25 | ``` 26 | 27 | ### Documentation ### 28 | 29 | Lorem -------------------------------------------------------------------------------- /thirdparty/threePostProcess/shaders/LuminosityHighPassShader.js: -------------------------------------------------------------------------------- 1 | import { 2 | Color 3 | } from 'three'; 4 | 5 | /** 6 | * Luminosity 7 | * http://en.wikipedia.org/wiki/Luminosity 8 | */ 9 | 10 | const LuminosityHighPassShader = { 11 | 12 | shaderID: 'luminosityHighPass', 13 | 14 | uniforms: { 15 | 16 | 'tDiffuse': { value: null }, 17 | 'luminosityThreshold': { value: 1.0 }, 18 | 'smoothWidth': { value: 1.0 }, 19 | 'defaultColor': { value: new Color( 0x000000 ) }, 20 | 'defaultOpacity': { value: 0.0 } 21 | 22 | }, 23 | 24 | vertexShader: /* glsl */` 25 | 26 | varying vec2 vUv; 27 | 28 | void main() { 29 | 30 | vUv = uv; 31 | 32 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 33 | 34 | }`, 35 | 36 | fragmentShader: /* glsl */` 37 | 38 | uniform sampler2D tDiffuse; 39 | uniform vec3 defaultColor; 40 | uniform float defaultOpacity; 41 | uniform float luminosityThreshold; 42 | uniform float smoothWidth; 43 | 44 | varying vec2 vUv; 45 | 46 | void main() { 47 | 48 | vec4 texel = texture2D( tDiffuse, vUv ); 49 | 50 | vec3 luma = vec3( 0.299, 0.587, 0.114 ); 51 | 52 | float v = dot( texel.xyz, luma ); 53 | 54 | vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity ); 55 | 56 | float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v ); 57 | 58 | gl_FragColor = mix( outputColor, texel, alpha ); 59 | 60 | }` 61 | 62 | }; 63 | 64 | export { LuminosityHighPassShader }; 65 | -------------------------------------------------------------------------------- /thirdparty/notes.txt: -------------------------------------------------------------------------------- 1 | https://cdnjs.com/libraries/three.js 2 | https://cdnjs.cloudflare.com/ajax/libs/three.js/0.157.0/three.module.min.js 3 | https://cdnjs.cloudflare.com/ajax/libs/three.js/0.157.0/three.module.js // Modified 4 | comment out define SHADER_TYPE 5 | find _this.properties = properties; and Add 6 | _this.attributes = attributes; 7 | 8 | https://cdn.jsdelivr.net/npm/three@0.157.0/ 9 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/controls/OrbitControls.js 10 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/loaders/GLTFLoader.js 11 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/utils/BufferGeometryUtils.js 12 | 13 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/postprocessing/RenderPass.js 14 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/postprocessing/Pass.js 15 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/postprocessing/EffectComposer.js // MODIFIED, shader import 16 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/postprocessing/MaskPass.js 17 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/postprocessing/ShaderPass.js 18 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/shaders/CopyShader.js 19 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/postprocessing/OutputPass.js // MODIFIED, shader imort 20 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/shaders/OutputShader.js 21 | 22 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/postprocessing/UnrealBloomPass.js // MODIFIED, Shader imports 23 | https://cdn.jsdelivr.net/npm/three@0.157.0/examples/jsm/shaders/LuminosityHighPassShader.js -------------------------------------------------------------------------------- /thirdparty/threePostProcess/shaders/OutputShader.js: -------------------------------------------------------------------------------- 1 | import { 2 | ShaderChunk 3 | } from 'three'; 4 | 5 | const OutputShader = { 6 | 7 | uniforms: { 8 | 9 | 'tDiffuse': { value: null }, 10 | 'toneMappingExposure': { value: 1 } 11 | 12 | }, 13 | 14 | vertexShader: /* glsl */` 15 | precision highp float; 16 | 17 | uniform mat4 modelViewMatrix; 18 | uniform mat4 projectionMatrix; 19 | 20 | attribute vec3 position; 21 | attribute vec2 uv; 22 | 23 | varying vec2 vUv; 24 | 25 | void main() { 26 | 27 | vUv = uv; 28 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 29 | 30 | }`, 31 | 32 | fragmentShader: /* glsl */` 33 | 34 | precision highp float; 35 | 36 | uniform sampler2D tDiffuse; 37 | 38 | ` + ShaderChunk[ 'tonemapping_pars_fragment' ] + ShaderChunk[ 'colorspace_pars_fragment' ] + ` 39 | 40 | varying vec2 vUv; 41 | 42 | void main() { 43 | 44 | gl_FragColor = texture2D( tDiffuse, vUv ); 45 | 46 | // tone mapping 47 | 48 | #ifdef LINEAR_TONE_MAPPING 49 | 50 | gl_FragColor.rgb = LinearToneMapping( gl_FragColor.rgb ); 51 | 52 | #elif defined( REINHARD_TONE_MAPPING ) 53 | 54 | gl_FragColor.rgb = ReinhardToneMapping( gl_FragColor.rgb ); 55 | 56 | #elif defined( CINEON_TONE_MAPPING ) 57 | 58 | gl_FragColor.rgb = OptimizedCineonToneMapping( gl_FragColor.rgb ); 59 | 60 | #elif defined( ACES_FILMIC_TONE_MAPPING ) 61 | 62 | gl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb ); 63 | 64 | #endif 65 | 66 | // color space 67 | 68 | #ifdef SRGB_TRANSFER 69 | 70 | gl_FragColor = sRGBTransferOETF( gl_FragColor ); 71 | 72 | #endif 73 | 74 | }` 75 | 76 | }; 77 | 78 | export { OutputShader }; 79 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /prototypes/002_voxel_grid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /thirdparty/threePostProcess/ShaderPass.js: -------------------------------------------------------------------------------- 1 | import { 2 | ShaderMaterial, 3 | UniformsUtils 4 | } from 'three'; 5 | import { Pass, FullScreenQuad } from './Pass.js'; 6 | 7 | class ShaderPass extends Pass { 8 | 9 | constructor( shader, textureID ) { 10 | 11 | super(); 12 | 13 | this.textureID = ( textureID !== undefined ) ? textureID : 'tDiffuse'; 14 | 15 | if ( shader instanceof ShaderMaterial ) { 16 | 17 | this.uniforms = shader.uniforms; 18 | 19 | this.material = shader; 20 | 21 | } else if ( shader ) { 22 | 23 | this.uniforms = UniformsUtils.clone( shader.uniforms ); 24 | 25 | this.material = new ShaderMaterial( { 26 | 27 | name: ( shader.name !== undefined ) ? shader.name : 'unspecified', 28 | defines: Object.assign( {}, shader.defines ), 29 | uniforms: this.uniforms, 30 | vertexShader: shader.vertexShader, 31 | fragmentShader: shader.fragmentShader 32 | 33 | } ); 34 | 35 | } 36 | 37 | this.fsQuad = new FullScreenQuad( this.material ); 38 | 39 | } 40 | 41 | render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) { 42 | 43 | if ( this.uniforms[ this.textureID ] ) { 44 | 45 | this.uniforms[ this.textureID ].value = readBuffer.texture; 46 | 47 | } 48 | 49 | this.fsQuad.material = this.material; 50 | 51 | if ( this.renderToScreen ) { 52 | 53 | renderer.setRenderTarget( null ); 54 | this.fsQuad.render( renderer ); 55 | 56 | } else { 57 | 58 | renderer.setRenderTarget( writeBuffer ); 59 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 60 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil ); 61 | this.fsQuad.render( renderer ); 62 | 63 | } 64 | 65 | } 66 | 67 | dispose() { 68 | 69 | this.material.dispose(); 70 | 71 | this.fsQuad.dispose(); 72 | 73 | } 74 | 75 | } 76 | 77 | export { ShaderPass }; 78 | -------------------------------------------------------------------------------- /thirdparty/threePostProcess/Pass.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferGeometry, 3 | Float32BufferAttribute, 4 | OrthographicCamera, 5 | Mesh 6 | } from 'three'; 7 | 8 | class Pass { 9 | 10 | constructor() { 11 | 12 | this.isPass = true; 13 | 14 | // if set to true, the pass is processed by the composer 15 | this.enabled = true; 16 | 17 | // if set to true, the pass indicates to swap read and write buffer after rendering 18 | this.needsSwap = true; 19 | 20 | // if set to true, the pass clears its buffer before rendering 21 | this.clear = false; 22 | 23 | // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer. 24 | this.renderToScreen = false; 25 | 26 | } 27 | 28 | setSize( /* width, height */ ) {} 29 | 30 | render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) { 31 | 32 | console.error( 'THREE.Pass: .render() must be implemented in derived pass.' ); 33 | 34 | } 35 | 36 | dispose() {} 37 | 38 | } 39 | 40 | // Helper for passes that need to fill the viewport with a single quad. 41 | 42 | const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 43 | 44 | // https://github.com/mrdoob/three.js/pull/21358 45 | 46 | const _geometry = new BufferGeometry(); 47 | _geometry.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) ); 48 | _geometry.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) ); 49 | 50 | class FullScreenQuad { 51 | 52 | constructor( material ) { 53 | 54 | this._mesh = new Mesh( _geometry, material ); 55 | 56 | } 57 | 58 | dispose() { 59 | 60 | this._mesh.geometry.dispose(); 61 | 62 | } 63 | 64 | render( renderer ) { 65 | 66 | renderer.render( this._mesh, _camera ); 67 | 68 | } 69 | 70 | get material() { 71 | 72 | return this._mesh.material; 73 | 74 | } 75 | 76 | set material( value ) { 77 | 78 | this._mesh.material = value; 79 | 80 | } 81 | 82 | } 83 | 84 | export { Pass, FullScreenQuad }; 85 | -------------------------------------------------------------------------------- /thirdparty/threePostProcess/RenderPass.js: -------------------------------------------------------------------------------- 1 | import { 2 | Color 3 | } from 'three'; 4 | import { Pass } from './Pass.js'; 5 | 6 | class RenderPass extends Pass { 7 | 8 | constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) { 9 | 10 | super(); 11 | 12 | this.scene = scene; 13 | this.camera = camera; 14 | 15 | this.overrideMaterial = overrideMaterial; 16 | 17 | this.clearColor = clearColor; 18 | this.clearAlpha = clearAlpha; 19 | 20 | this.clear = true; 21 | this.clearDepth = false; 22 | this.needsSwap = false; 23 | this._oldClearColor = new Color(); 24 | 25 | } 26 | 27 | render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) { 28 | 29 | const oldAutoClear = renderer.autoClear; 30 | renderer.autoClear = false; 31 | 32 | let oldClearAlpha, oldOverrideMaterial; 33 | 34 | if ( this.overrideMaterial !== null ) { 35 | 36 | oldOverrideMaterial = this.scene.overrideMaterial; 37 | 38 | this.scene.overrideMaterial = this.overrideMaterial; 39 | 40 | } 41 | 42 | if ( this.clearColor !== null ) { 43 | 44 | renderer.getClearColor( this._oldClearColor ); 45 | renderer.setClearColor( this.clearColor ); 46 | 47 | } 48 | 49 | if ( this.clearAlpha !== null ) { 50 | 51 | oldClearAlpha = renderer.getClearAlpha(); 52 | renderer.setClearAlpha( this.clearAlpha ); 53 | 54 | } 55 | 56 | if ( this.clearDepth == true ) { 57 | 58 | renderer.clearDepth(); 59 | 60 | } 61 | 62 | renderer.setRenderTarget( this.renderToScreen ? null : readBuffer ); 63 | 64 | if ( this.clear === true ) { 65 | 66 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 67 | renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil ); 68 | 69 | } 70 | 71 | renderer.render( this.scene, this.camera ); 72 | 73 | // restore 74 | 75 | if ( this.clearColor !== null ) { 76 | 77 | renderer.setClearColor( this._oldClearColor ); 78 | 79 | } 80 | 81 | if ( this.clearAlpha !== null ) { 82 | 83 | renderer.setClearAlpha( oldClearAlpha ); 84 | 85 | } 86 | 87 | if ( this.overrideMaterial !== null ) { 88 | 89 | this.scene.overrideMaterial = oldOverrideMaterial; 90 | 91 | } 92 | 93 | renderer.autoClear = oldAutoClear; 94 | 95 | } 96 | 97 | } 98 | 99 | export { RenderPass }; 100 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import useThreeWebGL2, { THREE } from './lib/useThreeWebGL2.js'; 2 | import usePostEffects from './lib/usePostEffects.js'; 3 | import useDarkScene from './lib/useDarkScene.js'; 4 | 5 | import BoxBorderMaterial from './lib/material/BoxBorderMaterial.js'; 6 | 7 | export default class App{ 8 | // #region MAIN 9 | constructor( props={} ){ 10 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 11 | props = Object.assign( { 12 | postEffects : false, 13 | }, props ); 14 | 15 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 16 | if( !props.postEffects ){ 17 | this.three = useDarkScene( useThreeWebGL2( { colorMode:true }) ); 18 | 19 | }else{ 20 | this.three = useDarkScene( usePostEffects( useThreeWebGL2( { colorMode:true }) ) ); 21 | addEffects( this.three ); 22 | } 23 | 24 | this.renderLoop = this.three.createRenderLoop( this.onPreRender ); 25 | 26 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 27 | this.three.sphericalLook( 40, 20, 25 ); 28 | 29 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 30 | 31 | const geo = new THREE.BoxGeometry( 1, 1, 1 ); 32 | let mesh = new THREE.Mesh( geo, BoxBorderMaterial() ); 33 | this.three.scene.add( mesh ); 34 | 35 | } 36 | 37 | onPreRender = ( dt, et )=>{}; 38 | // #endregion 39 | 40 | } 41 | 42 | 43 | // #region POSTEFFECT 44 | import UnrealBloomPass from 'postprocess/UnrealBloomPass.js'; 45 | import OutputPass from 'postprocess/OutputPass.js'; 46 | function addEffects( tjs ){ 47 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 48 | // tjs.composer.renderToScreen = false; 49 | tjs.renderer.toneMapping = THREE.ReinhardToneMapping; 50 | tjs.renderer.toneMappingExposure = Math.pow( 1, 4.0 ); 51 | 52 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 53 | const res = tjs.getRenderSize(); 54 | const bloomPass = new UnrealBloomPass( new THREE.Vector2( res[0], res[1] ) ); 55 | bloomPass.threshold = 0; 56 | bloomPass.strength = 0.5; 57 | bloomPass.radius = 0.1; 58 | 59 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 60 | const outputPass = new OutputPass(); 61 | 62 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 63 | tjs.composer.addPass( bloomPass ); 64 | tjs.composer.addPass( outputPass ); 65 | } 66 | // #endregion -------------------------------------------------------------------------------- /src/lib/usePostEffects.js: -------------------------------------------------------------------------------- 1 | // #region IMPORTS 2 | // import * as THREE from 'three'; 3 | import { EffectComposer } from 'postprocess/EffectComposer.js'; 4 | import { RenderPass } from 'postprocess/RenderPass.js'; 5 | // #endregion 6 | 7 | // #region MAIN 8 | export default function usePostEffects( tjs ){ 9 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | // RENDERER 11 | tjs.renderer.setClearColor( 0x000000, 0 ); // Make the background blank 12 | 13 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 14 | // POST EFFECTS 15 | const composer = new EffectComposer( tjs.renderer ); 16 | composer.renderToScreen = true; 17 | 18 | const renderPass = new RenderPass( tjs.scene, tjs.camera ); 19 | composer.addPass( renderPass ); 20 | 21 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 22 | // METHODS 23 | const render = ( onPreRender=null, onPostRender=null ) =>{ 24 | const deltaTime = tjs.clock.getDelta(); 25 | const ellapseTime = tjs.clock.getElapsedTime(); 26 | 27 | if( onPreRender ) onPreRender( deltaTime, ellapseTime ); 28 | 29 | composer.render(); 30 | 31 | if( onPostRender ) onPostRender( deltaTime, ellapseTime ); 32 | return tjs; 33 | }; 34 | 35 | const renderLoop = ()=>{ 36 | window.requestAnimationFrame( renderLoop ); 37 | render(); 38 | return tjs; 39 | }; 40 | 41 | const createRenderLoop = ( fnPreRender=null, fnPostRender=null )=>{ 42 | let reqId = 0; 43 | 44 | const onRender = ()=>{ 45 | render( fnPreRender, fnPostRender ); 46 | reqId = window.requestAnimationFrame( onRender ); 47 | }; 48 | 49 | return { 50 | stop : () => window.cancelAnimationFrame( reqId ), 51 | start : () => onRender(), 52 | }; 53 | }; 54 | 55 | const onResize = ( e )=>{ composer.setSize( e.detail.width, e.detail.height ); }; 56 | 57 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | // Replace render controls with one that handles composer 59 | tjs.render = render; 60 | tjs.renderLoop = renderLoop; 61 | tjs.createRenderLoop = createRenderLoop; 62 | tjs.composer = composer; 63 | 64 | // Handle resizing the composer 65 | tjs.events.on( 'resize', onResize ); 66 | 67 | // Set size of composer 68 | const res = tjs.getRenderSize(); 69 | composer.setSize( res[0], res[1] ); 70 | 71 | return tjs; 72 | } 73 | // #endregion -------------------------------------------------------------------------------- /thirdparty/threePostProcess/OutputPass.js: -------------------------------------------------------------------------------- 1 | import { 2 | ColorManagement, 3 | RawShaderMaterial, 4 | UniformsUtils, 5 | LinearToneMapping, 6 | ReinhardToneMapping, 7 | CineonToneMapping, 8 | ACESFilmicToneMapping, 9 | SRGBTransfer 10 | } from 'three'; 11 | import { Pass, FullScreenQuad } from './Pass.js'; 12 | import { OutputShader } from './shaders/OutputShader.js'; 13 | 14 | class OutputPass extends Pass { 15 | 16 | constructor() { 17 | 18 | super(); 19 | 20 | // 21 | 22 | const shader = OutputShader; 23 | 24 | this.uniforms = UniformsUtils.clone( shader.uniforms ); 25 | 26 | this.material = new RawShaderMaterial( { 27 | uniforms: this.uniforms, 28 | vertexShader: shader.vertexShader, 29 | fragmentShader: shader.fragmentShader 30 | } ); 31 | 32 | this.fsQuad = new FullScreenQuad( this.material ); 33 | 34 | // internal cache 35 | 36 | this._outputColorSpace = null; 37 | this._toneMapping = null; 38 | 39 | } 40 | 41 | render( renderer, writeBuffer, readBuffer/*, deltaTime, maskActive */ ) { 42 | 43 | this.uniforms[ 'tDiffuse' ].value = readBuffer.texture; 44 | this.uniforms[ 'toneMappingExposure' ].value = renderer.toneMappingExposure; 45 | 46 | // rebuild defines if required 47 | 48 | if ( this._outputColorSpace !== renderer.outputColorSpace || this._toneMapping !== renderer.toneMapping ) { 49 | 50 | this._outputColorSpace = renderer.outputColorSpace; 51 | this._toneMapping = renderer.toneMapping; 52 | 53 | this.material.defines = {}; 54 | 55 | if ( ColorManagement.getTransfer( this._outputColorSpace ) === SRGBTransfer ) this.material.defines.SRGB_TRANSFER = ''; 56 | 57 | if ( this._toneMapping === LinearToneMapping ) this.material.defines.LINEAR_TONE_MAPPING = ''; 58 | else if ( this._toneMapping === ReinhardToneMapping ) this.material.defines.REINHARD_TONE_MAPPING = ''; 59 | else if ( this._toneMapping === CineonToneMapping ) this.material.defines.CINEON_TONE_MAPPING = ''; 60 | else if ( this._toneMapping === ACESFilmicToneMapping ) this.material.defines.ACES_FILMIC_TONE_MAPPING = ''; 61 | 62 | this.material.needsUpdate = true; 63 | 64 | } 65 | 66 | // 67 | 68 | if ( this.renderToScreen === true ) { 69 | 70 | renderer.setRenderTarget( null ); 71 | this.fsQuad.render( renderer ); 72 | 73 | } else { 74 | 75 | renderer.setRenderTarget( writeBuffer ); 76 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil ); 77 | this.fsQuad.render( renderer ); 78 | 79 | } 80 | 81 | } 82 | 83 | dispose() { 84 | 85 | this.material.dispose(); 86 | this.fsQuad.dispose(); 87 | 88 | } 89 | 90 | } 91 | 92 | export default OutputPass; 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /thirdparty/threePostProcess/MaskPass.js: -------------------------------------------------------------------------------- 1 | import { Pass } from './Pass.js'; 2 | 3 | class MaskPass extends Pass { 4 | 5 | constructor( scene, camera ) { 6 | 7 | super(); 8 | 9 | this.scene = scene; 10 | this.camera = camera; 11 | 12 | this.clear = true; 13 | this.needsSwap = false; 14 | 15 | this.inverse = false; 16 | 17 | } 18 | 19 | render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) { 20 | 21 | const context = renderer.getContext(); 22 | const state = renderer.state; 23 | 24 | // don't update color or depth 25 | 26 | state.buffers.color.setMask( false ); 27 | state.buffers.depth.setMask( false ); 28 | 29 | // lock buffers 30 | 31 | state.buffers.color.setLocked( true ); 32 | state.buffers.depth.setLocked( true ); 33 | 34 | // set up stencil 35 | 36 | let writeValue, clearValue; 37 | 38 | if ( this.inverse ) { 39 | 40 | writeValue = 0; 41 | clearValue = 1; 42 | 43 | } else { 44 | 45 | writeValue = 1; 46 | clearValue = 0; 47 | 48 | } 49 | 50 | state.buffers.stencil.setTest( true ); 51 | state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE ); 52 | state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff ); 53 | state.buffers.stencil.setClear( clearValue ); 54 | state.buffers.stencil.setLocked( true ); 55 | 56 | // draw into the stencil buffer 57 | 58 | renderer.setRenderTarget( readBuffer ); 59 | if ( this.clear ) renderer.clear(); 60 | renderer.render( this.scene, this.camera ); 61 | 62 | renderer.setRenderTarget( writeBuffer ); 63 | if ( this.clear ) renderer.clear(); 64 | renderer.render( this.scene, this.camera ); 65 | 66 | // unlock color and depth buffer and make them writable for subsequent rendering/clearing 67 | 68 | state.buffers.color.setLocked( false ); 69 | state.buffers.depth.setLocked( false ); 70 | 71 | state.buffers.color.setMask( true ); 72 | state.buffers.depth.setMask( true ); 73 | 74 | // only render where stencil is set to 1 75 | 76 | state.buffers.stencil.setLocked( false ); 77 | state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1 78 | state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP ); 79 | state.buffers.stencil.setLocked( true ); 80 | 81 | } 82 | 83 | } 84 | 85 | class ClearMaskPass extends Pass { 86 | 87 | constructor() { 88 | 89 | super(); 90 | 91 | this.needsSwap = false; 92 | 93 | } 94 | 95 | render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) { 96 | 97 | renderer.state.buffers.stencil.setLocked( false ); 98 | renderer.state.buffers.stencil.setTest( false ); 99 | 100 | } 101 | 102 | } 103 | 104 | export { MaskPass, ClearMaskPass }; 105 | -------------------------------------------------------------------------------- /bs-config.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | |-------------------------------------------------------------------------- 4 | | Browser-sync config file 5 | |-------------------------------------------------------------------------- 6 | | 7 | | For up-to-date information about the options: 8 | | http://www.browsersync.io/docs/options/ 9 | | 10 | | There are more options than you see here, these are just the ones that are 11 | | set internally. See the website for more info. 12 | | 13 | | 14 | */ 15 | module.exports = { 16 | //"ui": { "port": 3333 }, 17 | "ui": false, 18 | "files": [ './**/*.{html,htm,css,js}' ], 19 | "watchEvents": [ "change" ], 20 | "watch": false, 21 | "ignore": [], 22 | "single": false, 23 | "watchOptions": { 24 | "ignoreInitial": true, 25 | "ignored": 'node_modules' 26 | }, 27 | "server": { 28 | baseDir : './', 29 | directory: true 30 | }, 31 | "proxy": false, 32 | "port": 1337, 33 | "middleware": false, 34 | "serveStatic": [], 35 | "ghostMode": { 36 | "clicks": true, 37 | "scroll": true, 38 | "location": true, 39 | "forms": { 40 | "submit": true, 41 | "inputs": true, 42 | "toggles": true 43 | } 44 | }, 45 | "logLevel": "info", 46 | "logPrefix": "Browsersync", 47 | "logConnections": false, 48 | "logFileChanges": true, 49 | "logSnippet": true, 50 | "rewriteRules": [], 51 | "open": "local", 52 | "browser": "default", 53 | "cors": false, 54 | "xip": false, 55 | "hostnameSuffix": false, 56 | "reloadOnRestart": false, 57 | "notify": true, 58 | "scrollProportionally": true, 59 | "scrollThrottle": 0, 60 | "scrollRestoreTechnique": "window.name", 61 | "scrollElements": [], 62 | "scrollElementMapping": [], 63 | "reloadDelay": 0, 64 | "reloadDebounce": 500, 65 | "reloadThrottle": 0, 66 | "plugins": [], 67 | "injectChanges": false, 68 | "startPath": null, 69 | "minify": true, 70 | "host": null, 71 | "localOnly": false, 72 | "codeSync": true, 73 | "timestamps": true, 74 | "clientEvents": [ 75 | "scroll", 76 | "scroll:element", 77 | "input:text", 78 | "input:toggles", 79 | "form:submit", 80 | "form:reset", 81 | "click" 82 | ], 83 | "socket": { 84 | "socketIoOptions": { 85 | "log": false 86 | }, 87 | "socketIoClientConfig": { 88 | "reconnectionAttempts": 50 89 | }, 90 | "path": "/browser-sync/socket.io", 91 | "clientPath": "/browser-sync", 92 | "namespace": "/browser-sync", 93 | "clients": { 94 | "heartbeatTimeout": 5000 95 | } 96 | }, 97 | "tagNames": { 98 | "less": "link", 99 | "scss": "link", 100 | "css": "link", 101 | "jpg": "img", 102 | "jpeg": "img", 103 | "png": "img", 104 | "svg": "img", 105 | "gif": "img", 106 | "js": "script" 107 | }, 108 | "injectNotification": false 109 | }; -------------------------------------------------------------------------------- /prototypes/000_pieces.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/lib/material/BoxBorderMaterial_v2.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | /** Material to draw a border around each face of a cube */ 4 | export default function BoxBorderMaterial( props={} ){ 5 | props = Object.assign( { 6 | borderSize : 0.01, 7 | faceColor : 0xe0e0e0, 8 | faceAlpha : 0.6, 9 | borderColor : null, 10 | borderAlpha : 1.0, 11 | }, props ); 12 | 13 | 14 | const mat = new THREE.RawShaderMaterial({ 15 | depthTest : true, 16 | transparent : true, 17 | alphaToCoverage : true, 18 | side : THREE.DoubleSide, 19 | 20 | uniforms: { 21 | borderSize : { type: 'float', value: props.borderSize }, 22 | borderColor : { 23 | type : 'vec3', 24 | value : new THREE.Color( props.borderColor !== null ? props.borderColor : props.faceColor ), 25 | }, 26 | 27 | borderAlpha : { type: 'float', value: props.borderAlpha }, 28 | faceColor : { type: 'vec3', value: new THREE.Color( props.faceColor ) }, 29 | faceAlpha : { type: 'float', value: props.faceAlpha }, 30 | }, 31 | 32 | extensions: { derivatives: true, }, 33 | 34 | vertexShader: `#version 300 es 35 | in vec3 position; 36 | in vec3 normal; 37 | in vec2 uv; 38 | 39 | uniform mat4 modelMatrix; 40 | uniform mat4 viewMatrix; 41 | uniform mat4 projectionMatrix; 42 | 43 | flat out vec3 fragMaxLPos; 44 | flat out vec3 fragLNorm; 45 | out vec3 fragScaleLPos; 46 | 47 | vec3 decomposeScaleFromMat4( mat4 m ){ 48 | return vec3( 49 | length( vec3( m[0][0], m[0][1], m[0][2] ) ), 50 | length( vec3( m[1][0], m[1][1], m[1][2] ) ), 51 | length( vec3( m[2][0], m[2][1], m[2][2] ) ) 52 | ); 53 | } 54 | 55 | void main(){ 56 | vec4 wPos = modelMatrix * vec4( position, 1.0 ); // World Space 57 | vec4 vPos = viewMatrix * wPos; // View Space 58 | gl_Position = projectionMatrix * vPos; 59 | 60 | /* ORIGIN AT CENTER 61 | // Scaled Localspace Position 62 | fragScaleLPos = position * decomposeScaleFromMat4( modelMatrix ); 63 | 64 | // Non-Interpolated values 65 | fragMaxLPos = abs( fragScaleLPos ); 66 | fragLNorm = abs( normal ); 67 | */ 68 | 69 | /* ORGIN AT BOTTOM TOP CORNER */ 70 | fragMaxLPos = decomposeScaleFromMat4( modelMatrix ); 71 | fragScaleLPos = position * fragMaxLPos; 72 | fragLNorm = abs( normal ); 73 | }`, 74 | 75 | fragmentShader: `#version 300 es 76 | precision mediump float; 77 | 78 | uniform float borderSize; 79 | uniform vec3 borderColor; 80 | uniform float borderAlpha; 81 | uniform vec3 faceColor; 82 | uniform float faceAlpha; 83 | 84 | flat in vec3 fragMaxLPos; 85 | flat in vec3 fragLNorm; 86 | in vec3 fragScaleLPos; 87 | 88 | out vec4 outColor; 89 | 90 | void main(){ 91 | /* 92 | // USED FOR CUBES'S ORIGIN AT CENTER 93 | vec3 absPos = abs( fragScaleLPos ); // Absolute Scaled Position to handle negative axes 94 | vec3 px = fwidth( absPos ); // Pixel Difference 95 | 96 | // Use normal to filter out specific axis, ex: Front face, its normal is [0,0,1] 97 | // We only need XY to draw border, so adding normal makes sure Z get a higher value by 98 | // adding 1 to its results value while adding 0 to the others. Using the MIN function will 99 | // end up selecting either X or Y since it'll have the smallest value & filter out Z. 100 | 101 | vec3 vMask = fragLNorm + smoothstep( fragMaxLPos - borderSize, fragMaxLPos - borderSize - px, absPos ); 102 | float mask = 1.0 - min( min( vMask.x, vMask.y ), vMask.z ); 103 | 104 | outColor = mix( vec4( faceColor, faceAlpha ), vec4( borderColor, borderAlpha ), mask ); 105 | */ 106 | 107 | // USED FOR CUBE"S ORIGIN AT BOTTOM LEFT CORNER 108 | vec3 maxPos = fragMaxLPos * 0.5; // MaxLPos is the actual scale value 109 | vec3 absPos = abs( fragScaleLPos - maxPos ); // Halfing max, we can then get Pos from Center 110 | vec3 px = fwidth( absPos ); 111 | 112 | // The rest of the code follows the original 113 | vec3 vMask = fragLNorm + smoothstep( maxPos - borderSize, maxPos - borderSize - px, absPos ); 114 | float mask = 1.0 - min( min( vMask.x, vMask.y ), vMask.z ); 115 | 116 | outColor = mix( vec4( faceColor, faceAlpha ), vec4( borderColor, borderAlpha ), mask ); 117 | }`, 118 | }); 119 | 120 | Object.defineProperty(mat, 'faceColor', { 121 | set( c ){ mat.uniforms.faceColor.value.set( c ); }, 122 | }); 123 | 124 | Object.defineProperty(mat, 'borderSize', { 125 | set( v ){ mat.uniforms.borderSize.value = v; }, 126 | }); 127 | 128 | return mat; 129 | } 130 | -------------------------------------------------------------------------------- /thirdparty/threePostProcess/EffectComposer.js: -------------------------------------------------------------------------------- 1 | import { 2 | Clock, 3 | HalfFloatType, 4 | NoBlending, 5 | Vector2, 6 | WebGLRenderTarget 7 | } from 'three'; 8 | import { CopyShader } from './shaders/CopyShader.js'; 9 | import { ShaderPass } from './ShaderPass.js'; 10 | import { MaskPass } from './MaskPass.js'; 11 | import { ClearMaskPass } from './MaskPass.js'; 12 | 13 | class EffectComposer { 14 | 15 | constructor( renderer, renderTarget ) { 16 | 17 | this.renderer = renderer; 18 | 19 | this._pixelRatio = renderer.getPixelRatio(); 20 | 21 | if ( renderTarget === undefined ) { 22 | 23 | const size = renderer.getSize( new Vector2() ); 24 | this._width = size.width; 25 | this._height = size.height; 26 | 27 | renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } ); 28 | renderTarget.texture.name = 'EffectComposer.rt1'; 29 | 30 | } else { 31 | 32 | this._width = renderTarget.width; 33 | this._height = renderTarget.height; 34 | 35 | } 36 | 37 | this.renderTarget1 = renderTarget; 38 | this.renderTarget2 = renderTarget.clone(); 39 | this.renderTarget2.texture.name = 'EffectComposer.rt2'; 40 | 41 | this.writeBuffer = this.renderTarget1; 42 | this.readBuffer = this.renderTarget2; 43 | 44 | this.renderToScreen = true; 45 | 46 | this.passes = []; 47 | 48 | this.copyPass = new ShaderPass( CopyShader ); 49 | this.copyPass.material.blending = NoBlending; 50 | 51 | this.clock = new Clock(); 52 | 53 | } 54 | 55 | swapBuffers() { 56 | 57 | const tmp = this.readBuffer; 58 | this.readBuffer = this.writeBuffer; 59 | this.writeBuffer = tmp; 60 | 61 | } 62 | 63 | addPass( pass ) { 64 | 65 | this.passes.push( pass ); 66 | pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio ); 67 | 68 | } 69 | 70 | insertPass( pass, index ) { 71 | 72 | this.passes.splice( index, 0, pass ); 73 | pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio ); 74 | 75 | } 76 | 77 | removePass( pass ) { 78 | 79 | const index = this.passes.indexOf( pass ); 80 | 81 | if ( index !== - 1 ) { 82 | 83 | this.passes.splice( index, 1 ); 84 | 85 | } 86 | 87 | } 88 | 89 | isLastEnabledPass( passIndex ) { 90 | 91 | for ( let i = passIndex + 1; i < this.passes.length; i ++ ) { 92 | 93 | if ( this.passes[ i ].enabled ) { 94 | 95 | return false; 96 | 97 | } 98 | 99 | } 100 | 101 | return true; 102 | 103 | } 104 | 105 | render( deltaTime ) { 106 | 107 | // deltaTime value is in seconds 108 | 109 | if ( deltaTime === undefined ) { 110 | 111 | deltaTime = this.clock.getDelta(); 112 | 113 | } 114 | 115 | const currentRenderTarget = this.renderer.getRenderTarget(); 116 | 117 | let maskActive = false; 118 | 119 | for ( let i = 0, il = this.passes.length; i < il; i ++ ) { 120 | 121 | const pass = this.passes[ i ]; 122 | 123 | if ( pass.enabled === false ) continue; 124 | 125 | pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) ); 126 | pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive ); 127 | 128 | if ( pass.needsSwap ) { 129 | 130 | if ( maskActive ) { 131 | 132 | const context = this.renderer.getContext(); 133 | const stencil = this.renderer.state.buffers.stencil; 134 | 135 | //context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff ); 136 | stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff ); 137 | 138 | this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime ); 139 | 140 | //context.stencilFunc( context.EQUAL, 1, 0xffffffff ); 141 | stencil.setFunc( context.EQUAL, 1, 0xffffffff ); 142 | 143 | } 144 | 145 | this.swapBuffers(); 146 | 147 | } 148 | 149 | if ( MaskPass !== undefined ) { 150 | 151 | if ( pass instanceof MaskPass ) { 152 | 153 | maskActive = true; 154 | 155 | } else if ( pass instanceof ClearMaskPass ) { 156 | 157 | maskActive = false; 158 | 159 | } 160 | 161 | } 162 | 163 | } 164 | 165 | this.renderer.setRenderTarget( currentRenderTarget ); 166 | 167 | } 168 | 169 | reset( renderTarget ) { 170 | 171 | if ( renderTarget === undefined ) { 172 | 173 | const size = this.renderer.getSize( new Vector2() ); 174 | this._pixelRatio = this.renderer.getPixelRatio(); 175 | this._width = size.width; 176 | this._height = size.height; 177 | 178 | renderTarget = this.renderTarget1.clone(); 179 | renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio ); 180 | 181 | } 182 | 183 | this.renderTarget1.dispose(); 184 | this.renderTarget2.dispose(); 185 | this.renderTarget1 = renderTarget; 186 | this.renderTarget2 = renderTarget.clone(); 187 | 188 | this.writeBuffer = this.renderTarget1; 189 | this.readBuffer = this.renderTarget2; 190 | 191 | } 192 | 193 | setSize( width, height ) { 194 | 195 | this._width = width; 196 | this._height = height; 197 | 198 | const effectiveWidth = this._width * this._pixelRatio; 199 | const effectiveHeight = this._height * this._pixelRatio; 200 | 201 | this.renderTarget1.setSize( effectiveWidth, effectiveHeight ); 202 | this.renderTarget2.setSize( effectiveWidth, effectiveHeight ); 203 | 204 | for ( let i = 0; i < this.passes.length; i ++ ) { 205 | 206 | this.passes[ i ].setSize( effectiveWidth, effectiveHeight ); 207 | 208 | } 209 | 210 | } 211 | 212 | setPixelRatio( pixelRatio ) { 213 | 214 | this._pixelRatio = pixelRatio; 215 | 216 | this.setSize( this._width, this._height ); 217 | 218 | } 219 | 220 | dispose() { 221 | 222 | this.renderTarget1.dispose(); 223 | this.renderTarget2.dispose(); 224 | 225 | this.copyPass.dispose(); 226 | 227 | } 228 | 229 | } 230 | 231 | export { EffectComposer }; 232 | -------------------------------------------------------------------------------- /prototypes/001_grid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/lib/misc/VoxelGrid.js: -------------------------------------------------------------------------------- 1 | // #region CONSTANTS 2 | const NEIGHBOR_OFFSETS = [ 3 | [0,0,1], [0,0,-1], 4 | [0,1,0], [0,-1,0], 5 | [1,0,0], [-1,0,0], 6 | ]; 7 | // #endregion 8 | 9 | export default class VoxelGrid{ 10 | // #region MAIN 11 | cellState = null; // On/Off state for each voxel 12 | cellSize = 1; // Size of the voxel 13 | xzCount = 0; // How many voxels for one Y row 14 | 15 | dimension = [0,0,0]; // How many voxels per axis 16 | maxCoord = [0,0,0]; // Max coordinate per axis 17 | minBound = [0,0,0]; // Min Bounding location 18 | maxBound = [0,0,0]; // Max Bounding location 19 | 20 | constructor( x=2, y=2, z=2 ){ 21 | this.setDimension( x, y, z ); 22 | 23 | } 24 | // #endregion 25 | 26 | // #region SETTERS 27 | setCellSize( s ){ 28 | this.cellSize = s; 29 | // this.minBound[0] = 0; 30 | // this.minBound[1] = 0; 31 | // this.minBound[2] = 0; 32 | this.maxBound[0] = this.dimension[0] * this.cellSize; 33 | this.maxBound[1] = this.dimension[1] * this.cellSize; 34 | this.maxBound[2] = this.dimension[2] * this.cellSize; 35 | return this; 36 | } 37 | 38 | setDimension( x, y, z ){ 39 | this.xzCount = x * z; 40 | this.dimension[0] = x; 41 | this.dimension[1] = y; 42 | this.dimension[2] = z; 43 | 44 | this.maxCoord[0] = x-1; 45 | this.maxCoord[1] = y-1; 46 | this.maxCoord[2] = z-1; 47 | 48 | this.minBound[0] = 0; 49 | this.minBound[1] = 0; 50 | this.minBound[2] = 0; 51 | 52 | this.maxBound[0] = x * this.cellSize; 53 | this.maxBound[1] = y * this.cellSize; 54 | this.maxBound[2] = z * this.cellSize; 55 | 56 | this.cellState = new Uint8Array( 57 | this.dimension[0] * 58 | this.dimension[2] * 59 | this.dimension[1] 60 | ); 61 | } 62 | // #endregion 63 | 64 | // #region COORD MATH 65 | 66 | // Using Voxel Coordinates, Gets the Cell Array Index 67 | coordIdx( coord ){ 68 | // ( xLen * zLen * y ) + ( xLen * z ) + x 69 | const x = Math.min( Math.max( coord[0], 0 ), maxCoord[0] ); 70 | const y = Math.min( Math.max( coord[1], 0 ), maxCoord[1] ); 71 | const z = Math.min( Math.max( coord[2], 0 ), maxCoord[2] ); 72 | return this.xzCount * y + this.dimension[0] * z + x; 73 | } 74 | 75 | // Using Cell Array Index, Compute Voxel Coordinate 76 | idxCoord( i, out=[0,0,0] ){ 77 | const y = Math.floor( i / this.xzCount ); // How Many Y Levels Can We Get? 78 | const xz = i - y * this.xzCount; // Subtract Y Levels from total, To get remaining Layer 79 | const z = Math.floor( xz / this.dimension[0] ); // How many rows in the last layer can we get? 80 | 81 | out[0] = xz - z * this.dimension[0]; 82 | out[1] = y; 83 | out[2] = z; 84 | return out; 85 | } 86 | 87 | // Convert Worldspace Position to Voxel Coordinates 88 | posCoord( pos, out=[0,0,0] ){ 89 | // Localize Postion in relation to Chunk's Starting position 90 | // Divide the Local Position by Voxel's Size. 91 | // Floor it to get final coordinate value. 92 | out[0] = Math.floor( (pos[0] - this.minBound[0]) / this.cellSize ); 93 | out[1] = Math.floor( (pos[1] - this.minBound[1]) / this.cellSize ); 94 | out[2] = Math.floor( (pos[2] - this.minBound[2]) / this.cellSize ); 95 | return out; 96 | } 97 | 98 | // Get the cell min/max boundary from voxel coordinates 99 | coordBound( coord, minOut, maxOut ){ 100 | minOut[0] = coord[0] * this.cellSize + this.minBound[0]; 101 | minOut[1] = coord[1] * this.cellSize + this.minBound[1]; 102 | minOut[2] = coord[2] * this.cellSize + this.minBound[2]; 103 | 104 | maxOut[0] = ( coord[0] + 1 ) * this.cellSize + this.minBound[0]; 105 | maxOut[1] = ( coord[1] + 1 ) * this.cellSize + this.minBound[1]; 106 | maxOut[2] = ( coord[2] + 1 ) * this.cellSize + this.minBound[2]; 107 | } 108 | 109 | // Get the cell min boundary from voxel coordinates 110 | coordMinBound( coord, minOut=[0,0,0] ){ 111 | minOut[0] = coord[0] * this.cellSize + this.minBound[0]; 112 | minOut[1] = coord[1] * this.cellSize + this.minBound[1]; 113 | minOut[2] = coord[2] * this.cellSize + this.minBound[2]; 114 | return minOut; 115 | } 116 | 117 | // Get the center point of a cell 118 | coordMidPoint( coord, out=[0,0,0] ){ 119 | const h = this.cellSize * 0.5; 120 | out[0] = coord[0] * this.cellSize + this.minBound[0] + h; 121 | out[1] = coord[1] * this.cellSize + this.minBound[1] + h; 122 | out[2] = coord[2] * this.cellSize + this.minBound[2] + h; 123 | return out; 124 | } 125 | 126 | isCoord( coord ){ 127 | if( coord[0] < 0 || coord[0] > this.maxCoord[0] ) return false; 128 | if( coord[1] < 0 || coord[1] > this.maxCoord[1] ) return false; 129 | if( coord[2] < 0 || coord[2] > this.maxCoord[2] ) return false; 130 | return true; 131 | } 132 | 133 | // #endregion 134 | 135 | // #region ITER 136 | 137 | // Loop over all the cells 138 | iterCells(){ 139 | let i = 0; 140 | const sCell = this.cellState; 141 | const len = sCell.length; 142 | const val = { 143 | min : [0,0,0], 144 | max : [0,0,0], 145 | coord : [0,0,0], 146 | isOn : false, 147 | idx : 0, 148 | }; 149 | 150 | const result = { done: false, value: val }; 151 | const next = ()=>{ 152 | if( i >= len ) result.done = true; 153 | else{ 154 | val.idx = i; // Cell Index 155 | val.isOn = ( sCell[ i ] != 0 ); // Is Cell Active 156 | this.idxCoord( i++, val.coord ); // Compute Voxel Coordinate 157 | this.coordBound( val.coord, val.min, val.max ); // cell Bounding 158 | } 159 | return result; 160 | }; 161 | 162 | return { [Symbol.iterator]() { return { next }; } }; 163 | } 164 | 165 | // #endregion 166 | } -------------------------------------------------------------------------------- /src/lib/meshes/Cube.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | // Create a cube with its origin at the bottom left back corner 4 | export default class Cube{ 5 | static mesh( props = {mat:null, pos:null, scl:null, size:[1,1,1], offset:[-0.5,-0.5,-0.5]} ){ 6 | const geo = this.get( props.size, props.offset ); 7 | const bGeo = new THREE.BufferGeometry(); 8 | bGeo.setIndex( new THREE.BufferAttribute( geo.indices, 1 ) ); 9 | bGeo.setAttribute( 'position', new THREE.BufferAttribute( geo.vertices, 3 ) ); 10 | bGeo.setAttribute( 'normal', new THREE.BufferAttribute( geo.normals, 3 ) ); 11 | bGeo.setAttribute( 'uv', new THREE.BufferAttribute( geo.texcoord, 2 ) ); 12 | 13 | return bGeo; 14 | // const mesh = new THREE.Mesh( bGeo, props.mat || new THREE.MeshPhongMaterial( { color:0x009999 } ) ); 15 | // if( props.pos ) mesh.position.fromArray( props.pos ); 16 | // if( props.scl != null ) mesh.scale.set( props.scl, props.scl, props.scl ); 17 | 18 | // return mesh; 19 | } 20 | 21 | static lineMesh( props = {mat:null, pos:null, scl:null, size:[1,1,1], offset:[-0.5,-0.5,-0.5]} ){ 22 | const geo = this.getLine( props.size, props.offset ); 23 | const bGeo = new THREE.BufferGeometry(); 24 | bGeo.setIndex( new THREE.BufferAttribute( geo.indices, 1 ) ); 25 | bGeo.setAttribute( 'position', new THREE.BufferAttribute( geo.vertices, 3 ) ); 26 | 27 | const mesh = new THREE.LineSegments( bGeo, props.mat || new THREE.LineBasicMaterial( { color: 0x00ffff } ) ); 28 | if( props.pos ) mesh.position.fromArray( props.pos ); 29 | if( props.scl != null ) mesh.scale.set( props.scl, props.scl, props.scl ); 30 | return mesh; 31 | } 32 | 33 | static corner( props={mat:null, pos:null, scl:null, isLine:false } ){ 34 | props.size = [1,1,1]; 35 | props.offset = [0,0,0]; 36 | return ( props.isLine )? this.lineMesh( props ) : this.mesh( props ); 37 | } 38 | 39 | static floor( props={mat:null, pos:null, scl:null, isLine:false } ){ 40 | props.size = [1,1,1]; 41 | props.offset = [-0.5,0,-0.5]; 42 | return ( props.isLine )? this.lineMesh( props ) : this.mesh( props ); 43 | } 44 | 45 | static get( size=[1,1,1], offset=[-0.5,-0.5,0.5] ){ 46 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 47 | const x1 = size[0] + offset[ 0 ], 48 | y1 = size[1] + offset[ 1 ], 49 | z1 = size[2] + offset[ 2 ], 50 | x0 = offset[ 0 ], 51 | y0 = offset[ 1 ], 52 | z0 = offset[ 2 ]; 53 | 54 | // Starting bottom left corner, then working counter clockwise to create the front face. 55 | // Backface is the first face but in reverse (3,2,1,0) 56 | // keep each quad face built the same way to make index and uv easier to assign 57 | const vert = [ 58 | x0, y1, z1, //0 Front 59 | x0, y0, z1, //1 60 | x1, y0, z1, //2 61 | x1, y1, z1, //3 62 | 63 | x1, y1, z0, //4 Back 64 | x1, y0, z0, //5 65 | x0, y0, z0, //6 66 | x0, y1, z0, //7 67 | 68 | x1, y1, z1, //3 Right 69 | x1, y0, z1, //2 70 | x1, y0, z0, //5 71 | x1, y1, z0, //4 72 | 73 | x0, y0, z1, //1 Bottom 74 | x0, y0, z0, //6 75 | x1, y0, z0, //5 76 | x1, y0, z1, //2 77 | 78 | x0, y1, z0, //7 Left 79 | x0, y0, z0, //6 80 | x0, y0, z1, //1 81 | x0, y1, z1, //0 82 | 83 | x0, y1, z0, //7 Top 84 | x0, y1, z1, //0 85 | x1, y1, z1, //3 86 | x1, y1, z0, //4 87 | ]; 88 | 89 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 90 | //Build the index of each quad [0,1,2, 2,3,0] 91 | let i; 92 | const idx = []; 93 | for( i=0; i < vert.length / 3; i+=2) idx.push( i, i+1, ( Math.floor( i / 4 ) * 4 ) + ( ( i + 2 ) % 4 ) ); 94 | 95 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 96 | //Build UV data for each vertex 97 | const uv = []; 98 | for( i=0; i < 6; i++) uv.push( 0,0, 0,1, 1,1, 1,0 ); 99 | 100 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 101 | 102 | return { 103 | vertices : new Float32Array( vert ), 104 | indices : new Uint16Array( idx ), 105 | texcoord : new Float32Array( uv ), 106 | normals : new Float32Array( [ // Left/Right have their xNormal flipped to render correctly in 3JS, Why does normals need to be mirrored on X? 107 | 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, //Front 108 | 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, //Back 109 | 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, //Left 110 | 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, //Bottom 111 | -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, //Right 112 | 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0 //Top 113 | ] ), 114 | }; 115 | } 116 | 117 | static getLine( size=[1,1,1], offset=[-0.5,-0.5,-0.5] ){ 118 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 119 | const x1 = size[0] + offset[ 0 ], 120 | y1 = size[1] + offset[ 1 ], 121 | z1 = size[2] + offset[ 2 ], 122 | x0 = offset[ 0 ], 123 | y0 = offset[ 1 ], 124 | z0 = offset[ 2 ]; 125 | 126 | // Starting bottom left corner, then working counter clockwise to create the front face. 127 | // Backface is the first face but in reverse (3,2,1,0) 128 | // keep each quad face built the same way to make index and uv easier to assign 129 | 130 | return { 131 | vertices : new Float32Array( [ 132 | x0, y1, z1, //0 Front 133 | x0, y0, z1, //1 134 | x1, y0, z1, //2 135 | x1, y1, z1, //3 136 | 137 | x1, y1, z0, //4 Back 138 | x1, y0, z0, //5 139 | x0, y0, z0, //6 140 | x0, y1, z0, //7 141 | ] ), 142 | 143 | indices : new Uint16Array( [ 144 | 0,1, 1,2, 2,3, 3,0, 145 | 4,5, 5,6, 6,7, 7,4, 146 | 0,7, 1,6, 2,5, 3,4, 147 | ] ), 148 | }; 149 | } 150 | } -------------------------------------------------------------------------------- /prototypes/003_rotate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 227 | 230 |
231 | 232 | 233 | 242 |
243 | -------------------------------------------------------------------------------- /src/lib/useThreeWebGL2.js: -------------------------------------------------------------------------------- 1 | // #region IMPORTS 2 | import * as THREE from 'three'; 3 | import { OrbitControls } from 'OrbitControls'; 4 | export { THREE }; 5 | // #endregion 6 | 7 | /* 8 | 12 | 13 | const App = useThreeWebGL2(); 14 | App.scene.add( facedCube( [0,3,0], 6 ) ); 15 | App 16 | .sphericalLook( 45, 35, 40 ) 17 | .renderLoop(); 18 | */ 19 | 20 | 21 | // #region OPTIONS 22 | export function useDarkScene( tjs, props ){ 23 | props = Object.assign({ floor:true }, props ); 24 | 25 | // Light 26 | const light = new THREE.DirectionalLight( 0xffffff, 0.8 ); 27 | light.position.set( 4, 10, 1 ); 28 | tjs.scene.add( light ); 29 | 30 | tjs.scene.add( new THREE.AmbientLight( 0x404040 ) ); 31 | 32 | // Floor 33 | if( props.floor ){ 34 | tjs.scene.add( new THREE.GridHelper( 20, 20, 0x0c610c, 0x444444 ) ); 35 | } 36 | 37 | // Renderer 38 | tjs.renderer.setClearColor( 0x3a3a3a, 1 ); 39 | return tjs; 40 | }; 41 | 42 | export async function useVisualDebug( tjs ){ 43 | const ary = await Promise.all([ 44 | import( './meshes/DynLineMesh.js' ), 45 | import( './meshes/ShapePointsMesh.js' ), 46 | ]); 47 | 48 | const o = {}; 49 | tjs.scene.add( ( o.ln = new ary[ 0 ].default ) ); 50 | tjs.scene.add( ( o.pnt = new ary[ 1 ].default ) ); 51 | return o; 52 | } 53 | // #endregion 54 | 55 | // #region EVENTS 56 | 57 | class EventDispatcher{ 58 | _evt = new EventTarget(); 59 | on( evtName, fn ){ this._evt.addEventListener( evtName, fn ); return this; } 60 | off( evtName, fn ){ this._evt.removeEventListener( evtName, fn ); return this; } 61 | once( evtName, fn ){ this._evt.addEventListener( evtName, fn, { once:true } ); return this; } 62 | emit( evtName, data=null ){ 63 | this._evt.dispatchEvent( ( !data ) 64 | ? new Event( evtName, { bubbles:false, cancelable:true, composed:false } ) 65 | : new CustomEvent( evtName, { detail:data, bubbles:false, cancelable:true, composed:false } ) 66 | ); 67 | return this; 68 | } 69 | } 70 | 71 | // #endregion 72 | 73 | // #region MAIN 74 | export default function useThreeWebGL2( props={} ){ 75 | props = Object.assign( { 76 | colorMode : false, 77 | shadows : false, 78 | preserverBuffer : false, 79 | power : '', 80 | }, props ); 81 | 82 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 83 | // RENDERER 84 | const options = { 85 | antialias : true, 86 | alpha : true, 87 | stencil : true, 88 | depth : true, 89 | preserveDrawingBuffer : props.preserverBuffer, 90 | powerPreference : ( props.power === '') ? 'default' : 91 | ( props.power === 'high' ) ? 'high-performance' : 'low-power', 92 | }; 93 | 94 | const canvas = document.createElement( 'canvas' ); 95 | options.canvas = canvas; 96 | options.context = canvas.getContext( 'webgl2' ); 97 | 98 | const renderer = new THREE.WebGLRenderer( options ); 99 | renderer.setPixelRatio( window.devicePixelRatio ); 100 | renderer.setClearColor( 0x3a3a3a, 1 ); 101 | 102 | //if( props.preserveDrawingBuffer ){ 103 | // renderer.autoClearColor = false; 104 | // renderer.autoClearDepth = false; 105 | // Manual clearing : r.clearColor(); r.clearDepth(); 106 | //} 107 | 108 | if( props.colorMode ){ 109 | // React-Fiber changes the default settings, the defaults can cause issues trying to map colors 1:1 110 | // https://docs.pmnd.rs/react-three-fiber/api/canvas#render-defaults 111 | // https://threejs.org/docs/#manual/en/introduction/Color-management 112 | renderer.outputColorSpace = THREE.SRGBColorSpace; // Turns on sRGB Encoding & Gamma Correction 113 | renderer.toneMapping = THREE.ACESFilmicToneMapping; // Try to make it close to HDR 114 | THREE.ColorManagement.enabled = true; // Turns old 3JS's old color manager 115 | } 116 | 117 | if( props.shadows ){ 118 | renderer.shadowMap.enabled = true; 119 | renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap 120 | } 121 | 122 | document.body.appendChild( renderer.domElement ); 123 | 124 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 125 | // CORE 126 | const scene = new THREE.Scene(); 127 | const clock = new THREE.Clock(); 128 | clock.start(); 129 | 130 | const camera = new THREE.PerspectiveCamera( 45, 1.0, 0.01, 5000 ); 131 | camera.position.set( 0, 5, 20 ); 132 | 133 | const camCtrl = new OrbitControls( camera, renderer.domElement ); 134 | 135 | const events = new EventDispatcher(); 136 | 137 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 138 | // METHODS 139 | let self; // Need to declare before methods for it to be useable 140 | 141 | const render = ( onPreRender=null, onPostRender=null ) =>{ 142 | const deltaTime = clock.getDelta(); 143 | const ellapseTime = clock.getElapsedTime(); 144 | 145 | if( onPreRender ) onPreRender( deltaTime, ellapseTime ); 146 | renderer.render( scene, camera ); 147 | if( onPostRender ) onPostRender( deltaTime, ellapseTime ); 148 | 149 | return self; 150 | }; 151 | 152 | const renderLoop = ()=>{ 153 | window.requestAnimationFrame( renderLoop ); 154 | render(); 155 | return self; 156 | }; 157 | 158 | const createRenderLoop = ( fnPreRender=null, fnPostRender=null )=>{ 159 | let reqId = 0; 160 | 161 | const onRender = ()=>{ 162 | render( fnPreRender, fnPostRender ); 163 | reqId = window.requestAnimationFrame( onRender ); 164 | }; 165 | 166 | return { 167 | stop : () => window.cancelAnimationFrame( reqId ), 168 | start : () => onRender(), 169 | }; 170 | }; 171 | 172 | const sphericalLook = ( lon, lat, radius, target=null )=>{ 173 | const phi = ( 90 - lat ) * Math.PI / 180; 174 | const theta = ( lon + 180 ) * Math.PI / 180; 175 | 176 | camera.position.set( 177 | -(radius * Math.sin( phi ) * Math.sin(theta)), 178 | radius * Math.cos( phi ), 179 | -(radius * Math.sin( phi ) * Math.cos(theta)) 180 | ); 181 | 182 | if( target ) camCtrl.target.fromArray( target ); 183 | camCtrl.update(); 184 | return self; 185 | }; 186 | 187 | const resize = ( w=0, h=0 )=>{ 188 | const W = w || window.innerWidth; 189 | const H = h || window.innerHeight; 190 | renderer.setSize( W, H ); // Update Renderer 191 | 192 | if( !camera.isOrthographicCamera ){ 193 | camera.aspect = W / H; 194 | }else{ 195 | const h = camera.top; 196 | const w = h * ( W / H ); 197 | camera.left = -w; 198 | camera.right = w; 199 | camera.top = h; 200 | camera.bottom = -h; 201 | } 202 | 203 | camera.updateProjectionMatrix(); 204 | events.emit( 'resize', { width:W, height:h } ); 205 | return self; 206 | }; 207 | 208 | const getRenderSize = ()=>{ 209 | const v = new THREE.Vector2(); 210 | renderer.getSize( v ); 211 | return v.toArray(); 212 | } 213 | 214 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 215 | 216 | window.addEventListener( 'resize', ()=>resize() ); 217 | resize(); 218 | 219 | return self = { 220 | renderer, 221 | scene, 222 | camera, 223 | camCtrl, 224 | clock, 225 | events, 226 | 227 | render, 228 | renderLoop, 229 | createRenderLoop, 230 | getRenderSize, 231 | sphericalLook, 232 | resize, 233 | 234 | version: ()=>{ return THREE.REVISION; }, 235 | }; 236 | } 237 | // #endregion 238 | -------------------------------------------------------------------------------- /src/lib/meshes/DynLineMesh.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | class DynLineMesh extends THREE.LineSegments{ 4 | _defaultColor = 0x00ff00; 5 | _cnt = 0; 6 | _verts = []; 7 | _color = []; 8 | _config = []; 9 | _dirty = false; 10 | 11 | constructor( initSize = 20 ){ 12 | super( 13 | _newDynLineMeshGeometry( 14 | new Float32Array( initSize * 2 * 3 ), // Two Points for Each Line 15 | new Float32Array( initSize * 2 * 3 ), 16 | new Float32Array( initSize * 2 * 1 ), 17 | false 18 | ), 19 | newDynLineMeshMaterial() //new THREE.PointsMaterial( { color: 0xffffff, size:8, sizeAttenuation:false } ) 20 | ); 21 | 22 | this.geometry.setDrawRange( 0, 0 ); 23 | this.onBeforeRender = ()=>{ if( this._dirty ) this._updateGeometry(); } 24 | } 25 | 26 | reset(){ 27 | this._cnt = 0; 28 | this._verts.length = 0; 29 | this._color.length = 0; 30 | this._config.length = 0; 31 | this.geometry.setDrawRange( 0, 0 ); 32 | return this; 33 | } 34 | 35 | add( p0, p1, color0=this._defaultColor, color1=null, isDash=false ){ 36 | this._verts.push( p0[0], p0[1], p0[2], p1[0], p1[1], p1[2] ); 37 | this._color.push( ...glColor( color0 ), ...glColor( (color1 != null) ? color1:color0 ) ); 38 | 39 | if( isDash ){ 40 | const len = Math.sqrt( 41 | (p1[0] - p0[0]) ** 2 + 42 | (p1[1] - p0[1]) ** 2 + 43 | (p1[2] - p0[2]) ** 2 44 | ); 45 | this._config.push( 0, len ); 46 | }else{ 47 | this._config.push( 0, 0 ); 48 | } 49 | 50 | this._cnt++; 51 | this._dirty = true; 52 | return this; 53 | } 54 | 55 | box( v0, v1, col=this._defaultColor, is_dash=false ){ 56 | let x1 = v0[0], y1 = v0[1], z1 = v0[2], 57 | x2 = v1[0], y2 = v1[1], z2 = v1[2]; 58 | 59 | this.add( [x1,y1,z1], [x1,y1,z2], col, null, is_dash ); // Bottom 60 | this.add( [x1,y1,z2], [x2,y1,z2], col, null, is_dash ); 61 | this.add( [x2,y1,z2], [x2,y1,z1], col, null, is_dash ); 62 | this.add( [x2,y1,z1], [x1,y1,z1], col, null, is_dash ); 63 | this.add( [x1,y2,z1], [x1,y2,z2], col, null, is_dash ); // Top 64 | this.add( [x1,y2,z2], [x2,y2,z2], col, null, is_dash ); 65 | this.add( [x2,y2,z2], [x2,y2,z1], col, null, is_dash ); 66 | this.add( [x2,y2,z1], [x1,y2,z1], col, null, is_dash ); 67 | this.add( [x1,y1,z1], [x1,y2,z1], col, null, is_dash ); // Sides 68 | this.add( [x1,y1,z2], [x1,y2,z2], col, null, is_dash ); 69 | this.add( [x2,y1,z2], [x2,y2,z2], col, null, is_dash ); 70 | this.add( [x2,y1,z1], [x2,y2,z1], col, null, is_dash ); 71 | return this; 72 | } 73 | 74 | obb( c, x, y, z, col=this._defaultColor, is_dash=false ){ 75 | const ba = [ c[0] - x[0] + y[0] - z[0], c[1] - x[1] + y[1] - z[1], c[2] - x[2] + y[2] - z[2] ]; 76 | const bb = [ c[0] - x[0] - y[0] - z[0], c[1] - x[1] - y[1] - z[1], c[2] - x[2] - y[2] - z[2] ]; 77 | const bc = [ c[0] + x[0] - y[0] - z[0], c[1] + x[1] - y[1] - z[1], c[2] + x[2] - y[2] - z[2] ]; 78 | const bd = [ c[0] + x[0] + y[0] - z[0], c[1] + x[1] + y[1] - z[1], c[2] + x[2] + y[2] - z[2] ]; 79 | const fa = [ c[0] - x[0] + y[0] + z[0], c[1] - x[1] + y[1] + z[1], c[2] - x[2] + y[2] + z[2] ]; 80 | const fb = [ c[0] - x[0] - y[0] + z[0], c[1] - x[1] - y[1] + z[1], c[2] - x[2] - y[2] + z[2] ]; 81 | const fc = [ c[0] + x[0] - y[0] + z[0], c[1] + x[1] - y[1] + z[1], c[2] + x[2] - y[2] + z[2] ]; 82 | const fd = [ c[0] + x[0] + y[0] + z[0], c[1] + x[1] + y[1] + z[1], c[2] + x[2] + y[2] + z[2] ]; 83 | this.add( ba, bb, col, null, is_dash ); // Back 84 | this.add( bb, bc, col, null, is_dash ); 85 | this.add( bc, bd, col, null, is_dash ); 86 | this.add( bd, ba, col, null, is_dash ); 87 | this.add( fa, fb, col, null, is_dash ); // Front 88 | this.add( fb, fc, col, null, is_dash ); 89 | this.add( fc, fd, col, null, is_dash ); 90 | this.add( fd, fa, col, null, is_dash ); 91 | this.add( fa, ba, col, null, is_dash ); // Connect 92 | this.add( fb, bb, col, null, is_dash ); 93 | this.add( fc, bc, col, null, is_dash ); 94 | this.add( fd, bd, col, null, is_dash ); 95 | return this 96 | } 97 | 98 | circle( origin, xAxis, yAxis, radius, seg, col=ln_color, is_dash=false ){ 99 | const prevPos = [0,0,0]; 100 | const pos = [0,0,0]; 101 | const PI2 = Math.PI * 2; 102 | let rad = 0; 103 | 104 | planeCircle( origin, xAxis, yAxis, 0, radius, prevPos ); 105 | for( let i=1; i <= seg; i++ ){ 106 | rad = PI2 * ( i / seg ); 107 | planeCircle( origin, xAxis, yAxis, rad, radius, pos ); 108 | this.add( prevPos, pos, col, null, is_dash ); 109 | 110 | prevPos[0] = pos[0]; 111 | prevPos[1] = pos[1]; 112 | prevPos[2] = pos[2]; 113 | } 114 | } 115 | 116 | _updateGeometry(){ 117 | const geo = this.geometry; 118 | const bVerts = geo.attributes.position; 119 | const bColor = geo.attributes.color; //this.geometry.index; 120 | const bConfig = geo.attributes.config; 121 | 122 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 123 | if( this._verts.length > bVerts.array.length || 124 | this._color.length > bColor.array.length || 125 | this._config.length > bConfig.array.length 126 | ){ 127 | if( this.geometry ) this.geometry.dispose(); 128 | this.geometry = _newDynLineMeshGeometry( this._verts, this._color, this._config ); 129 | this._dirty = false; 130 | return; 131 | } 132 | 133 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 134 | bVerts.array.set( this._verts ); 135 | bVerts.count = this._verts.length / 3; 136 | bVerts.needsUpdate = true; 137 | 138 | bColor.array.set( this._color ); 139 | bColor.count = this._color.length / 3; 140 | bColor.needsUpdate = true; 141 | 142 | bConfig.array.set( this._config ); 143 | bConfig.count = this._config.length / 1; 144 | bConfig.needsUpdate = true; 145 | 146 | geo.setDrawRange( 0, bVerts.count ); 147 | geo.computeBoundingBox(); 148 | geo.computeBoundingSphere(); 149 | 150 | this._dirty = false; 151 | } 152 | } 153 | 154 | //#region SUPPORT 155 | function _newDynLineMeshGeometry( aVerts, aColor, aConfig, doCompute=true ){ 156 | //if( !( aVerts instanceof Float32Array) ) aVerts = new Float32Array( aVerts ); 157 | //if( !( aColor instanceof Float32Array) ) aColor = new Float32Array( aColor ); 158 | 159 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 160 | const bVerts = new THREE.Float32BufferAttribute( aVerts, 3 ); 161 | const bColor = new THREE.Float32BufferAttribute( aColor, 3 ); 162 | const bConfig = new THREE.Float32BufferAttribute( aConfig, 1 ); 163 | bVerts.setUsage( THREE.DynamicDrawUsage ); 164 | bColor.setUsage( THREE.DynamicDrawUsage ); 165 | bConfig.setUsage( THREE.DynamicDrawUsage ); 166 | 167 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 168 | const geo = new THREE.BufferGeometry(); 169 | geo.setAttribute( 'position', bVerts ); 170 | geo.setAttribute( 'color', bColor ); 171 | geo.setAttribute( 'config', bConfig ); 172 | 173 | if( doCompute ){ 174 | geo.computeBoundingSphere(); 175 | geo.computeBoundingBox(); 176 | } 177 | return geo; 178 | } 179 | 180 | function glColor( hex, out = null ){ 181 | const NORMALIZE_RGB = 1 / 255; 182 | out = out || [0,0,0]; 183 | 184 | out[0] = ( hex >> 16 & 255 ) * NORMALIZE_RGB; 185 | out[1] = ( hex >> 8 & 255 ) * NORMALIZE_RGB; 186 | out[2] = ( hex & 255 ) * NORMALIZE_RGB; 187 | 188 | return out; 189 | } 190 | //#endregion 191 | 192 | //#region SHADER 193 | 194 | function newDynLineMeshMaterial(){ 195 | return new THREE.RawShaderMaterial({ 196 | depthTest : true, 197 | transparent : true, 198 | uniforms : { 199 | dashSeg : { value : 1 / 0.07 }, 200 | dashDiv : { value : 0.4 }, 201 | }, 202 | vertexShader : `#version 300 es 203 | in vec3 position; 204 | in vec3 color; 205 | in float config; 206 | 207 | uniform mat4 modelViewMatrix; 208 | uniform mat4 projectionMatrix; 209 | uniform float u_scale; 210 | 211 | out vec3 fragColor; 212 | out float fragLen; 213 | 214 | void main(){ 215 | vec4 wPos = modelViewMatrix * vec4( position, 1.0 ); 216 | 217 | fragColor = color; 218 | fragLen = config; 219 | 220 | gl_Position = projectionMatrix * wPos; 221 | }`, 222 | fragmentShader : `#version 300 es 223 | precision mediump float; 224 | 225 | uniform float dashSeg; 226 | uniform float dashDiv; 227 | 228 | in vec3 fragColor; 229 | in float fragLen; 230 | out vec4 outColor; 231 | 232 | void main(){ 233 | float alpha = 1.0; 234 | if( fragLen > 0.0 ) alpha = step( dashDiv, fract( fragLen * dashSeg ) ); 235 | outColor = vec4( fragColor, alpha ); 236 | }`}); 237 | } 238 | 239 | //#endregion 240 | 241 | export default DynLineMesh; -------------------------------------------------------------------------------- /src/lib/meshes/ShapePointsMesh.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | class ShapePointsMesh extends THREE.Points{ 4 | _defaultShape = 1; 5 | _defaultSize = 6; 6 | _defaultColor = 0x00ff00; 7 | _cnt = 0; 8 | _verts = []; 9 | _color = []; 10 | _config = []; 11 | _dirty = false; 12 | 13 | constructor( initSize = 20 ){ 14 | super( 15 | _newShapePointsMeshGeometry( 16 | new Float32Array( initSize * 3 ), 17 | new Float32Array( initSize * 3 ), 18 | new Float32Array( initSize * 2 ), 19 | false 20 | ), 21 | newShapePointsMeshMaterial() //new THREE.PointsMaterial( { color: 0xffffff, size:8, sizeAttenuation:false } ) 22 | ); 23 | 24 | this.geometry.setDrawRange( 0, 0 ); 25 | this.onBeforeRender = ()=>{ if( this._dirty ) this._updateGeometry(); } 26 | } 27 | 28 | reset(){ 29 | this._cnt = 0; 30 | this._verts.length = 0; 31 | this._color.length = 0; 32 | this._config.length = 0; 33 | this.geometry.setDrawRange( 0, 0 ); 34 | return this; 35 | } 36 | 37 | add( pos, color = this._defaultColor, size = this._defaultSize, shape = this._defaultShape ){ 38 | this._verts.push( pos[0], pos[1], pos[2] ); 39 | 40 | if( Array.isArray( color ) ) this._color.push( ...color ); // Already GLSL encoded 41 | else this._color.push( ...glColor( color ) ); // Numeric Hex, need glsl encoding 42 | 43 | this._config.push( size, shape ); 44 | this._cnt++; 45 | this._dirty = true; 46 | return this; 47 | } 48 | 49 | getByteSize(){ 50 | const geo = this.geometry; 51 | let size = 0; 52 | for( const attr of Object.values( geo.attributes ) ){ 53 | size += attr.array.byteLength; 54 | } 55 | return size; 56 | } 57 | 58 | _updateGeometry(){ 59 | const geo = this.geometry; 60 | const bVerts = geo.attributes.position; 61 | const bColor = geo.attributes.color; //this.geometry.index; 62 | const bConfig = geo.attributes.config; 63 | 64 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 65 | if( this._verts.length > bVerts.array.length || 66 | this._color.length > bColor.array.length || 67 | this._config.length > bConfig.array.length 68 | ){ 69 | if( this.geometry ){ 70 | this.geometry.dispose(); 71 | this.geometry = null; 72 | } 73 | this.geometry = _newShapePointsMeshGeometry( this._verts, this._color, this._config ); 74 | this._dirty = false; 75 | 76 | return; 77 | } 78 | 79 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 80 | bVerts.array.set( this._verts ); 81 | bVerts.count = this._verts.length / 3; 82 | bVerts.needsUpdate = true; 83 | 84 | bColor.array.set( this._color ); 85 | bColor.count = this._color.length / 3; 86 | bColor.needsUpdate = true; 87 | 88 | bConfig.array.set( this._config ); 89 | bConfig.count = this._config.length / 2; 90 | bConfig.needsUpdate = true; 91 | 92 | geo.setDrawRange( 0, bVerts.count ); 93 | geo.computeBoundingBox(); 94 | geo.computeBoundingSphere(); 95 | 96 | this._dirty = false; 97 | } 98 | } 99 | 100 | // #region SUPPORT 101 | function _newShapePointsMeshGeometry( aVerts, aColor, aConfig, doCompute=true ){ 102 | //if( !( aVerts instanceof Float32Array) ) aVerts = new Float32Array( aVerts ); 103 | //if( !( aColor instanceof Float32Array) ) aColor = new Float32Array( aColor ); 104 | 105 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 106 | const bVerts = new THREE.Float32BufferAttribute( aVerts, 3 ); 107 | const bColor = new THREE.Float32BufferAttribute( aColor, 3 ); 108 | const bConfig = new THREE.Float32BufferAttribute( aConfig, 2 ); 109 | bVerts.setUsage( THREE.DynamicDrawUsage ); 110 | bColor.setUsage( THREE.DynamicDrawUsage ); 111 | bConfig.setUsage( THREE.DynamicDrawUsage ); 112 | 113 | // bVerts.count = aVerts.length / 3; 114 | // bVerts.needsUpdate = true; 115 | 116 | // bColor.count = aColor.length / 3; 117 | // bColor.needsUpdate = true; 118 | 119 | // bConfig.count = aConfig.length / 2; 120 | // bConfig.needsUpdate = true; 121 | 122 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 123 | const geo = new THREE.BufferGeometry(); 124 | geo.setAttribute( 'position', bVerts ); 125 | geo.setAttribute( 'color', bColor ); 126 | geo.setAttribute( 'config', bConfig ); 127 | 128 | geo.setDrawRange( 0, bVerts.count ); 129 | geo.needsUpdate = true; 130 | 131 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 132 | if( doCompute ){ 133 | geo.computeBoundingSphere(); 134 | geo.computeBoundingBox(); 135 | } 136 | 137 | return geo; 138 | } 139 | 140 | function glColor( hex, out = null ){ 141 | const NORMALIZE_RGB = 1 / 255; 142 | out = out || [0,0,0]; 143 | 144 | out[0] = ( hex >> 16 & 255 ) * NORMALIZE_RGB; 145 | out[1] = ( hex >> 8 & 255 ) * NORMALIZE_RGB; 146 | out[2] = ( hex & 255 ) * NORMALIZE_RGB; 147 | 148 | return out; 149 | } 150 | // #endregion 151 | 152 | // #region SHADER 153 | 154 | function newShapePointsMeshMaterial(){ 155 | 156 | return new THREE.RawShaderMaterial({ 157 | depthTest : true, 158 | transparent : true, 159 | alphaToCoverage : true, 160 | uniforms : { u_scale:{ value : 20.0 } }, 161 | vertexShader : `#version 300 es 162 | in vec3 position; 163 | in vec3 color; 164 | in vec2 config; 165 | 166 | uniform mat4 modelViewMatrix; 167 | uniform mat4 projectionMatrix; 168 | uniform float u_scale; 169 | 170 | out vec3 fragColor; 171 | flat out int fragShape; 172 | 173 | void main(){ 174 | vec4 wPos = modelViewMatrix * vec4( position.xyz, 1.0 ); 175 | 176 | fragColor = color; 177 | fragShape = int( config.y ); 178 | 179 | gl_Position = projectionMatrix * wPos; 180 | gl_PointSize = config.x * ( u_scale / -wPos.z ); 181 | 182 | // Get pnt to be World Space Size 183 | //gl_PointSize = view_port_size.y * projectionMatrix[1][5] * 1.0 / gl_Position.w; 184 | //gl_PointSize = view_port_size.y * projectionMatrix[1][1] * 1.0 / gl_Position.w; 185 | }`, 186 | fragmentShader : `#version 300 es 187 | precision mediump float; 188 | 189 | #define PI 3.14159265359 190 | #define PI2 6.28318530718 191 | 192 | in vec3 fragColor; 193 | flat in int fragShape; 194 | out vec4 outColor; 195 | 196 | float circle(){ 197 | vec2 coord = gl_PointCoord * 2.0 - 1.0; // v_uv * 2.0 - 1.0; 198 | float radius = dot( coord, coord ); 199 | float dxdy = fwidth( radius ); 200 | return smoothstep( 0.90 + dxdy, 0.90 - dxdy, radius ); 201 | } 202 | 203 | float ring( float inner ){ 204 | vec2 coord = gl_PointCoord * 2.0 - 1.0; 205 | float radius = dot( coord, coord ); 206 | float dxdy = fwidth( radius ); 207 | return smoothstep( inner - dxdy, inner + dxdy, radius ) - 208 | smoothstep( 1.0 - dxdy, 1.0 + dxdy, radius ); 209 | } 210 | 211 | float diamond(){ 212 | // http://www.numb3r23.net/2015/08/17/using-fwidth-for-distance-based-anti-aliasing/ 213 | const float radius = 0.5; 214 | 215 | float dst = dot( abs(gl_PointCoord-vec2(0.5)), vec2(1.0) ); 216 | float aaf = fwidth( dst ); 217 | return 1.0 - smoothstep( radius - aaf, radius, dst ); 218 | } 219 | 220 | float poly( int sides, float offset, float scale ){ 221 | // https://thebookofshaders.com/07/ 222 | vec2 coord = gl_PointCoord * 2.0 - 1.0; 223 | 224 | coord.y += offset; 225 | coord *= scale; 226 | 227 | float a = atan( coord.x, coord.y ) + PI; // Angle of Pixel 228 | float r = PI2 / float( sides ); // Radius of Pixel 229 | float d = cos( floor( 0.5 + a / r ) * r-a ) * length( coord ); 230 | float f = fwidth( d ); 231 | return smoothstep( 0.5, 0.5 - f, d ); 232 | } 233 | 234 | // signed distance to a n-star polygon with external angle en 235 | float sdStar( float r, int n, float m ){ // m=[2,n] 236 | vec2 p = vec2( gl_PointCoord.x, 1.0 - gl_PointCoord.y ) * 2.0 - 1.0; 237 | 238 | // these 4 lines can be precomputed for a given shape 239 | float an = 3.141593/float(n); 240 | float en = 3.141593/m; 241 | vec2 acs = vec2(cos(an),sin(an)); 242 | vec2 ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) and simplify, for regular polygon, 243 | 244 | // reduce to first sector 245 | float bn = mod(atan(p.x,p.y),2.0*an) - an; 246 | p = length(p)*vec2(cos(bn),abs(sin(bn))); 247 | 248 | // line sdf 249 | p -= r*acs; 250 | p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y); 251 | 252 | float dist = length(p)*sign(p.x); 253 | float f = fwidth( dist ); 254 | 255 | return smoothstep( 0.0, 0.0 - f, dist ); 256 | } 257 | 258 | 259 | void main(){ 260 | float alpha = 1.0; 261 | 262 | if( fragShape == 1 ) alpha = circle(); 263 | if( fragShape == 2 ) alpha = diamond(); 264 | if( fragShape == 3 ) alpha = poly( 3, 0.2, 1.0 ); // Triangle 265 | if( fragShape == 4 ) alpha = poly( 5, 0.0, 0.65 ); // Pentagram 266 | if( fragShape == 5 ) alpha = poly( 6, 0.0, 0.65 ); // Hexagon 267 | if( fragShape == 6 ) alpha = ring( 0.2 ); 268 | if( fragShape == 7 ) alpha = ring( 0.7 ); 269 | if( fragShape == 8 ) alpha = sdStar( 1.0, 3, 2.3 ); 270 | if( fragShape == 9 ) alpha = sdStar( 1.0, 6, 2.5 ); 271 | if( fragShape == 10 ) alpha = sdStar( 1.0, 4, 2.4 ); 272 | if( fragShape == 11 ) alpha = sdStar( 1.0, 5, 2.8 ); 273 | 274 | outColor = vec4( fragColor, alpha ); 275 | }`}); 276 | } 277 | 278 | // #endregion 279 | 280 | export default ShapePointsMesh; -------------------------------------------------------------------------------- /src/lib/misc/VoxelGrid_v1.js: -------------------------------------------------------------------------------- 1 | import Vec3 from '../Vec3.js'; 2 | 3 | interface IterCellAllInfo{ 4 | min : Vec3, 5 | max : Vec3, 6 | coord : Vec3, 7 | isOn : boolean, 8 | } 9 | 10 | interface IterCellInfo{ 11 | min : Vec3, 12 | max : Vec3, 13 | coord : Vec3, 14 | } 15 | 16 | const NEIGHBOR_OFFSETS = [ 17 | [0,0,1], [0,0,-1], 18 | [0,1,0], [0,-1,0], 19 | [1,0,0], [-1,0,0], 20 | ]; 21 | 22 | export default class VoxelGrid{ 23 | 24 | //#region MAIN 25 | _cellState !: Uint8Array; // On/Off set of each Cell 26 | _cellData : Array | null = null; // User Data for each cell 27 | cellSize = 0; // Size of 28 | xzCount = 0; // x cell cnt * z cell cnt 29 | dimension = new Vec3(); // How Many Cells available at each axis. 30 | maxCoord = new Vec3(); // Maximum Coord 31 | minBound = new Vec3(); // Min Position 32 | maxBound = new Vec3(); // Max Position 33 | 34 | constructor( cellSize ?: number ){ 35 | if( cellSize != undefined ) this.cellSize = cellSize; 36 | } 37 | //#endregion //////////////////////////////////////////////////////////// 38 | 39 | //#region SETUP 40 | 41 | setCellSize( n:number ): this{ this.cellSize = n; return this; } 42 | 43 | /** Compute a Min/Max Chunk Boundary that fits over another bounds by using cell size */ 44 | fitBound( bMin:TVec3, bMax:TVec3, overScale=1 ): this{ 45 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 46 | // Figure out how many voxels can be made in mesh bounding box 47 | const vsize = Vec3 48 | .sub( bMax, bMin ) // Get Length of Each Axis 49 | .scale( overScale ) // Pad some extra space 50 | .divScale( this.cellSize ) // How Many Cells Fit per Axis 51 | .ceil() // OverShoot 52 | .copyTo( this.dimension ) // Save Cell Counts 53 | .scale( this.cellSize ); // Actual Volume Size 54 | 55 | this.xzCount = this.dimension[0] * this.dimension[2]; 56 | this.maxCoord.fromSub( this.dimension, [1,1,1] ); 57 | 58 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 59 | // Set the starting volume 60 | this.minBound.xyz( 0, 0, 0 ); 61 | this.maxBound.copy( vsize ); 62 | 63 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | // Move Volume's Mid Point to the Mesh's Mid Point 65 | const aMid = Vec3.lerp( bMin, bMax, 0.5 ); 66 | const bMid = Vec3.lerp( this.minBound, this.maxBound, 0.5 ); 67 | const delta = Vec3.sub( bMid, aMid ); 68 | 69 | this.minBound.sub( delta ); 70 | this.maxBound.sub( delta ); 71 | 72 | this._buildStateArray(); 73 | return this; 74 | } 75 | 76 | /** Create a Chunk Boundary based on how many cells are needed and its size, plus the origin point optional */ 77 | asAxisBlock( cellSize: number, xCnt:number, yCnt:number, zCnt:number, origin ?: TVec3 ): this{ 78 | this.cellSize = cellSize; 79 | this.dimension.xyz( xCnt, yCnt, zCnt ); 80 | 81 | if( origin ) this.minBound.copy( origin ); 82 | else this.minBound.xyz( 0, 0, 0 ); 83 | 84 | const mx = cellSize * xCnt; 85 | const my = cellSize * yCnt; 86 | const mz = cellSize * zCnt; 87 | this.maxBound 88 | .xyz( mx, my, mz ) 89 | .add( this.minBound ); 90 | 91 | this.xzCount = this.dimension[0] * this.dimension[2]; 92 | 93 | return this; 94 | } 95 | 96 | _buildStateArray(): void{ 97 | this._cellState = new Uint8Array( this.dimension.x * this.dimension.z * this.dimension.y ); 98 | } 99 | 100 | //#endregion //////////////////////////////////////////////////////////// 101 | 102 | //#region SETTERS / GETTERS 103 | get cellCount():number { return ( this._cellState )? this._cellState.length : 0; } 104 | 105 | getStateArrayRef(): Uint8Array | null { 106 | return this._cellState; 107 | } 108 | 109 | setState( coord: TVec3, isOn: boolean ): this{ 110 | if( this._cellState ){ 111 | const idx = this.coordIdx( coord ); 112 | this._cellState[ idx ] = ( isOn )? 1 : 0; 113 | } 114 | return this; 115 | } 116 | 117 | getState( coord: TVec3 ): boolean{ 118 | if( this._cellState ){ 119 | const idx = this.coordIdx( coord ); 120 | return ( this._cellState[ idx ] == 1 ); 121 | } 122 | return false; 123 | } 124 | 125 | resetState(): this{ 126 | if( this._cellState ){ 127 | let i; 128 | for( i=0; i < this._cellState.length; i++ ) this._cellState[ i ] = 0; 129 | } 130 | return this; 131 | } 132 | 133 | getNeighbors( coord: TVec3 ): Array{ 134 | const rtn: Array = []; 135 | const x = coord[ 0 ]; 136 | const y = coord[ 1 ]; 137 | const z = coord[ 2 ]; 138 | 139 | if( z < this.maxCoord[2] ) rtn.push( [ x, y, z+1 ] ); // Forward 140 | if( z > 0 ) rtn.push( [ x, y, z-1 ] ); // Back 141 | 142 | if( x < this.maxCoord[0] ) rtn.push( [ x+1, y, z ] ); // Right 143 | if( x > 0 ) rtn.push( [ x-1, y, z ] ); // Left 144 | 145 | if( y < this.maxCoord[2] ) rtn.push( [ x, y+1, z ] ); // Up 146 | if( y > 0 ) rtn.push( [ x, y-1, z ] ); // Down 147 | 148 | return rtn; 149 | } 150 | 151 | getActiveNeighbors( coord: TVec3 ): Array{ 152 | const rtn: Array = []; 153 | const v = new Vec3(); 154 | let no; 155 | 156 | for( no of NEIGHBOR_OFFSETS ){ 157 | v.fromAdd( coord, no ); 158 | if( this.isCoord( v ) && this.getState( v ) ) rtn.push( v.toArray() ); 159 | } 160 | 161 | return rtn; 162 | } 163 | 164 | //#endregion 165 | 166 | //#region USER DATA 167 | /* 168 | prepareDataSpace(): this{ 169 | if( this._cellState && !this._cellData ){ 170 | this._cellData = new Array( this._cellState.length ); 171 | } 172 | return this; 173 | } 174 | */ 175 | //#endregion 176 | 177 | //#region COORDINATE MATH 178 | 179 | /** Using Voxel Coordinates, Gets the Cell Array Index */ 180 | coordIdx( coord: TVec3 ): number{ 181 | // ( xLen * zLen * y ) + ( xLen * z ) + x 182 | return this.xzCount * coord[1] + this.dimension[ 0 ] * coord[2] + coord[0]; 183 | } 184 | 185 | /** Using Cell Array Index, Compute Voxel Coordinate */ 186 | idxCoord( i: number, out ?: TVec3 ): TVec3{ 187 | const y = Math.floor( i / this.xzCount ); // How Many Y Levels Can We Get? 188 | const xz = i - y * this.xzCount; // Subtract Y Levels from total, To get remaining Layer 189 | const z = Math.floor( xz / this.dimension[0] ); // How many rows in the last layer can we get? 190 | 191 | out = out || [0,0,0]; 192 | out[0] = xz - z * this.dimension[0]; 193 | out[1] = y; 194 | out[2] = z; 195 | return out; 196 | } 197 | 198 | /** Convert Worldspace Position to Voxel Coordinates */ 199 | posCoord( pos: TVec3, out ?: Vec3 ): Vec3 { 200 | out = out || new Vec3(); 201 | 202 | out .fromSub( pos, this.minBound ) // Localize Postion in relation to Chunk's Starting position 203 | .divScale( this.cellSize ) // Divide the Local Position by Voxel's Size. 204 | .floor(); // Floor it to get final coordinate value. 205 | 206 | return out; 207 | } 208 | 209 | /** Get the cell min/max boundary from voxel coordinates */ 210 | coordBound( coord: TVec3, minOut: Vec3, maxOut: Vec3 ): void{ 211 | minOut .fromScale( coord, this.cellSize ) 212 | .add( this.minBound ); 213 | 214 | maxOut .fromAdd( coord, [1,1,1] ) 215 | .scale( this.cellSize ) 216 | .add( this.minBound ); 217 | } 218 | 219 | /** Get the cell min boundary from voxel coordinates */ 220 | coordMinBound( coord: TVec3, minOut: Vec3 ): void{ 221 | minOut .fromScale( coord, this.cellSize ) 222 | .add( this.minBound ); 223 | } 224 | 225 | /** Get the center point of a cell */ 226 | coordMidPoint( coord: TVec3, out: Vec3 ): void{ 227 | const h = this.cellSize * 0.5; 228 | out .fromScale( coord, this.cellSize ) 229 | .add( this.minBound ) 230 | .add( [h,h,h] ); 231 | } 232 | 233 | isCoord( coord: TVec3 ): boolean{ 234 | if( coord[0] < 0 || coord[0] > this.maxCoord[0] ) return false; 235 | if( coord[1] < 0 || coord[1] > this.maxCoord[1] ) return false; 236 | if( coord[2] < 0 || coord[2] > this.maxCoord[2] ) return false; 237 | return true; 238 | } 239 | 240 | //#endregion //////////////////////////////////////////////////////////// 241 | 242 | //#region ITER 243 | /** Loop over all the cells */ 244 | iterAllCells(): Iterable< IterCellAllInfo >{ 245 | let i = 0; 246 | const sCell = this._cellState; 247 | const len = sCell.length; 248 | 249 | const val : IterCellAllInfo = { 250 | min : new Vec3(), 251 | max : new Vec3(), 252 | coord : new Vec3(), 253 | isOn : false, 254 | }; 255 | 256 | const result = { done: false, value: val }; 257 | const next = ()=>{ 258 | if( i >= len ) result.done = true; 259 | else{ 260 | val.isOn = ( sCell[ i ] != 0 ); // Is Cell Active 261 | 262 | this.idxCoord( i++, val.coord ); // Compute Voxel Coordinate 263 | 264 | val.min // Compute Min Bounds for Cell 265 | .fromScale( val.coord, this.cellSize ) 266 | .add( this.minBound ); 267 | 268 | val.max // Compute Max Bounds for Cell 269 | .fromAdd( val.coord, [1,1,1] ) 270 | .scale( this.cellSize ) 271 | .add( this.minBound ); 272 | } 273 | return result; 274 | }; 275 | 276 | return { [Symbol.iterator]() { return { next }; } }; 277 | } 278 | 279 | /** Loop over only cells that are active */ 280 | iterActiveCells(): Iterable< IterCellInfo >{ 281 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 282 | // Get a list of cell indices that are currently active 283 | let ii:number; 284 | const indices : Array = []; 285 | const sCell = this._cellState; 286 | for( ii=0; ii < sCell.length; ii++ ){ 287 | if( sCell[ ii ] == 1 ) indices.push( ii ); 288 | } 289 | 290 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 291 | let i = 0; 292 | const len = indices.length; 293 | const val : IterCellInfo = { 294 | min : new Vec3(), 295 | max : new Vec3(), 296 | coord : new Vec3(), 297 | }; 298 | 299 | const result = { done: false, value: val }; 300 | 301 | const next = ()=>{ 302 | if( i >= len ) result.done = true; 303 | else{ 304 | ii = indices[ i ]; // Get Cell Index 305 | this.idxCoord( ii, val.coord ); // Compute Voxel Coordinate 306 | 307 | val.min // Compute Min Bounds for Cell 308 | .fromScale( val.coord, this.cellSize ) 309 | .add( this.minBound ); 310 | 311 | val.max // Compute Max Bounds for Cell 312 | .fromAdd( val.coord, [1,1,1] ) 313 | .scale( this.cellSize ) 314 | .add( this.minBound ); 315 | 316 | i++; 317 | } 318 | return result; 319 | }; 320 | 321 | return { [Symbol.iterator]() { return { next }; } }; 322 | } 323 | //#endregion //////////////////////////////////////////////////////////// 324 | } -------------------------------------------------------------------------------- /thirdparty/threePostProcess/UnrealBloomPass.js: -------------------------------------------------------------------------------- 1 | import { 2 | AdditiveBlending, 3 | Color, 4 | HalfFloatType, 5 | MeshBasicMaterial, 6 | ShaderMaterial, 7 | UniformsUtils, 8 | Vector2, 9 | Vector3, 10 | WebGLRenderTarget 11 | } from 'three'; 12 | import { Pass, FullScreenQuad } from './Pass.js'; 13 | import { CopyShader } from './shaders/CopyShader.js'; 14 | import { LuminosityHighPassShader } from './shaders/LuminosityHighPassShader.js'; 15 | 16 | /** 17 | * UnrealBloomPass is inspired by the bloom pass of Unreal Engine. It creates a 18 | * mip map chain of bloom textures and blurs them with different radii. Because 19 | * of the weighted combination of mips, and because larger blurs are done on 20 | * higher mips, this effect provides good quality and performance. 21 | * 22 | * Reference: 23 | * - https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/ 24 | */ 25 | class UnrealBloomPass extends Pass { 26 | 27 | constructor( resolution, strength, radius, threshold ) { 28 | 29 | super(); 30 | 31 | this.strength = ( strength !== undefined ) ? strength : 1; 32 | this.radius = radius; 33 | this.threshold = threshold; 34 | this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 ); 35 | 36 | // create color only once here, reuse it later inside the render function 37 | this.clearColor = new Color( 0, 0, 0 ); 38 | 39 | // render targets 40 | this.renderTargetsHorizontal = []; 41 | this.renderTargetsVertical = []; 42 | this.nMips = 5; 43 | let resx = Math.round( this.resolution.x / 2 ); 44 | let resy = Math.round( this.resolution.y / 2 ); 45 | 46 | this.renderTargetBright = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } ); 47 | this.renderTargetBright.texture.name = 'UnrealBloomPass.bright'; 48 | this.renderTargetBright.texture.generateMipmaps = false; 49 | 50 | for ( let i = 0; i < this.nMips; i ++ ) { 51 | 52 | const renderTargetHorizonal = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } ); 53 | 54 | renderTargetHorizonal.texture.name = 'UnrealBloomPass.h' + i; 55 | renderTargetHorizonal.texture.generateMipmaps = false; 56 | 57 | this.renderTargetsHorizontal.push( renderTargetHorizonal ); 58 | 59 | const renderTargetVertical = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } ); 60 | 61 | renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i; 62 | renderTargetVertical.texture.generateMipmaps = false; 63 | 64 | this.renderTargetsVertical.push( renderTargetVertical ); 65 | 66 | resx = Math.round( resx / 2 ); 67 | 68 | resy = Math.round( resy / 2 ); 69 | 70 | } 71 | 72 | // luminosity high pass material 73 | 74 | const highPassShader = LuminosityHighPassShader; 75 | this.highPassUniforms = UniformsUtils.clone( highPassShader.uniforms ); 76 | 77 | this.highPassUniforms[ 'luminosityThreshold' ].value = threshold; 78 | this.highPassUniforms[ 'smoothWidth' ].value = 0.01; 79 | 80 | this.materialHighPassFilter = new ShaderMaterial( { 81 | uniforms: this.highPassUniforms, 82 | vertexShader: highPassShader.vertexShader, 83 | fragmentShader: highPassShader.fragmentShader 84 | } ); 85 | 86 | // gaussian blur materials 87 | 88 | this.separableBlurMaterials = []; 89 | const kernelSizeArray = [ 3, 5, 7, 9, 11 ]; 90 | resx = Math.round( this.resolution.x / 2 ); 91 | resy = Math.round( this.resolution.y / 2 ); 92 | 93 | for ( let i = 0; i < this.nMips; i ++ ) { 94 | 95 | this.separableBlurMaterials.push( this.getSeperableBlurMaterial( kernelSizeArray[ i ] ) ); 96 | 97 | this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy ); 98 | 99 | resx = Math.round( resx / 2 ); 100 | 101 | resy = Math.round( resy / 2 ); 102 | 103 | } 104 | 105 | // composite material 106 | 107 | this.compositeMaterial = this.getCompositeMaterial( this.nMips ); 108 | this.compositeMaterial.uniforms[ 'blurTexture1' ].value = this.renderTargetsVertical[ 0 ].texture; 109 | this.compositeMaterial.uniforms[ 'blurTexture2' ].value = this.renderTargetsVertical[ 1 ].texture; 110 | this.compositeMaterial.uniforms[ 'blurTexture3' ].value = this.renderTargetsVertical[ 2 ].texture; 111 | this.compositeMaterial.uniforms[ 'blurTexture4' ].value = this.renderTargetsVertical[ 3 ].texture; 112 | this.compositeMaterial.uniforms[ 'blurTexture5' ].value = this.renderTargetsVertical[ 4 ].texture; 113 | this.compositeMaterial.uniforms[ 'bloomStrength' ].value = strength; 114 | this.compositeMaterial.uniforms[ 'bloomRadius' ].value = 0.1; 115 | 116 | const bloomFactors = [ 1.0, 0.8, 0.6, 0.4, 0.2 ]; 117 | this.compositeMaterial.uniforms[ 'bloomFactors' ].value = bloomFactors; 118 | 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 ) ]; 119 | this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors; 120 | 121 | // blend material 122 | 123 | const copyShader = CopyShader; 124 | 125 | this.copyUniforms = UniformsUtils.clone( copyShader.uniforms ); 126 | 127 | this.blendMaterial = new ShaderMaterial( { 128 | uniforms: this.copyUniforms, 129 | vertexShader: copyShader.vertexShader, 130 | fragmentShader: copyShader.fragmentShader, 131 | blending: AdditiveBlending, 132 | depthTest: false, 133 | depthWrite: false, 134 | transparent: true 135 | } ); 136 | 137 | this.enabled = true; 138 | this.needsSwap = false; 139 | 140 | this._oldClearColor = new Color(); 141 | this.oldClearAlpha = 1; 142 | 143 | this.basic = new MeshBasicMaterial(); 144 | 145 | this.fsQuad = new FullScreenQuad( null ); 146 | 147 | } 148 | 149 | dispose() { 150 | 151 | for ( let i = 0; i < this.renderTargetsHorizontal.length; i ++ ) { 152 | 153 | this.renderTargetsHorizontal[ i ].dispose(); 154 | 155 | } 156 | 157 | for ( let i = 0; i < this.renderTargetsVertical.length; i ++ ) { 158 | 159 | this.renderTargetsVertical[ i ].dispose(); 160 | 161 | } 162 | 163 | this.renderTargetBright.dispose(); 164 | 165 | // 166 | 167 | for ( let i = 0; i < this.separableBlurMaterials.length; i ++ ) { 168 | 169 | this.separableBlurMaterials[ i ].dispose(); 170 | 171 | } 172 | 173 | this.compositeMaterial.dispose(); 174 | this.blendMaterial.dispose(); 175 | this.basic.dispose(); 176 | 177 | // 178 | 179 | this.fsQuad.dispose(); 180 | 181 | } 182 | 183 | setSize( width, height ) { 184 | 185 | let resx = Math.round( width / 2 ); 186 | let resy = Math.round( height / 2 ); 187 | 188 | this.renderTargetBright.setSize( resx, resy ); 189 | 190 | for ( let i = 0; i < this.nMips; i ++ ) { 191 | 192 | this.renderTargetsHorizontal[ i ].setSize( resx, resy ); 193 | this.renderTargetsVertical[ i ].setSize( resx, resy ); 194 | 195 | this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy ); 196 | 197 | resx = Math.round( resx / 2 ); 198 | resy = Math.round( resy / 2 ); 199 | 200 | } 201 | 202 | } 203 | 204 | render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) { 205 | 206 | renderer.getClearColor( this._oldClearColor ); 207 | this.oldClearAlpha = renderer.getClearAlpha(); 208 | const oldAutoClear = renderer.autoClear; 209 | renderer.autoClear = false; 210 | 211 | renderer.setClearColor( this.clearColor, 0 ); 212 | 213 | if ( maskActive ) renderer.state.buffers.stencil.setTest( false ); 214 | 215 | // Render input to screen 216 | 217 | if ( this.renderToScreen ) { 218 | 219 | this.fsQuad.material = this.basic; 220 | this.basic.map = readBuffer.texture; 221 | 222 | renderer.setRenderTarget( null ); 223 | renderer.clear(); 224 | this.fsQuad.render( renderer ); 225 | 226 | } 227 | 228 | // 1. Extract Bright Areas 229 | 230 | this.highPassUniforms[ 'tDiffuse' ].value = readBuffer.texture; 231 | this.highPassUniforms[ 'luminosityThreshold' ].value = this.threshold; 232 | this.fsQuad.material = this.materialHighPassFilter; 233 | 234 | renderer.setRenderTarget( this.renderTargetBright ); 235 | renderer.clear(); 236 | this.fsQuad.render( renderer ); 237 | 238 | // 2. Blur All the mips progressively 239 | 240 | let inputRenderTarget = this.renderTargetBright; 241 | 242 | for ( let i = 0; i < this.nMips; i ++ ) { 243 | 244 | this.fsQuad.material = this.separableBlurMaterials[ i ]; 245 | 246 | this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = inputRenderTarget.texture; 247 | this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionX; 248 | renderer.setRenderTarget( this.renderTargetsHorizontal[ i ] ); 249 | renderer.clear(); 250 | this.fsQuad.render( renderer ); 251 | 252 | this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = this.renderTargetsHorizontal[ i ].texture; 253 | this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionY; 254 | renderer.setRenderTarget( this.renderTargetsVertical[ i ] ); 255 | renderer.clear(); 256 | this.fsQuad.render( renderer ); 257 | 258 | inputRenderTarget = this.renderTargetsVertical[ i ]; 259 | 260 | } 261 | 262 | // Composite All the mips 263 | 264 | this.fsQuad.material = this.compositeMaterial; 265 | this.compositeMaterial.uniforms[ 'bloomStrength' ].value = this.strength; 266 | this.compositeMaterial.uniforms[ 'bloomRadius' ].value = this.radius; 267 | this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors; 268 | 269 | renderer.setRenderTarget( this.renderTargetsHorizontal[ 0 ] ); 270 | renderer.clear(); 271 | this.fsQuad.render( renderer ); 272 | 273 | // Blend it additively over the input texture 274 | 275 | this.fsQuad.material = this.blendMaterial; 276 | this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetsHorizontal[ 0 ].texture; 277 | 278 | if ( maskActive ) renderer.state.buffers.stencil.setTest( true ); 279 | 280 | if ( this.renderToScreen ) { 281 | 282 | renderer.setRenderTarget( null ); 283 | this.fsQuad.render( renderer ); 284 | 285 | } else { 286 | 287 | renderer.setRenderTarget( readBuffer ); 288 | this.fsQuad.render( renderer ); 289 | 290 | } 291 | 292 | // Restore renderer settings 293 | 294 | renderer.setClearColor( this._oldClearColor, this.oldClearAlpha ); 295 | renderer.autoClear = oldAutoClear; 296 | 297 | } 298 | 299 | getSeperableBlurMaterial( kernelRadius ) { 300 | 301 | const coefficients = []; 302 | 303 | for ( let i = 0; i < kernelRadius; i ++ ) { 304 | 305 | coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( kernelRadius * kernelRadius ) ) / kernelRadius ); 306 | 307 | } 308 | 309 | return new ShaderMaterial( { 310 | 311 | defines: { 312 | 'KERNEL_RADIUS': kernelRadius 313 | }, 314 | 315 | uniforms: { 316 | 'colorTexture': { value: null }, 317 | 'invSize': { value: new Vector2( 0.5, 0.5 ) }, // inverse texture size 318 | 'direction': { value: new Vector2( 0.5, 0.5 ) }, 319 | 'gaussianCoefficients': { value: coefficients } // precomputed Gaussian coefficients 320 | }, 321 | 322 | vertexShader: 323 | `varying vec2 vUv; 324 | void main() { 325 | vUv = uv; 326 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 327 | }`, 328 | 329 | fragmentShader: 330 | `#include 331 | varying vec2 vUv; 332 | uniform sampler2D colorTexture; 333 | uniform vec2 invSize; 334 | uniform vec2 direction; 335 | uniform float gaussianCoefficients[KERNEL_RADIUS]; 336 | 337 | void main() { 338 | float weightSum = gaussianCoefficients[0]; 339 | vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum; 340 | for( int i = 1; i < KERNEL_RADIUS; i ++ ) { 341 | float x = float(i); 342 | float w = gaussianCoefficients[i]; 343 | vec2 uvOffset = direction * invSize * x; 344 | vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb; 345 | vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb; 346 | diffuseSum += (sample1 + sample2) * w; 347 | weightSum += 2.0 * w; 348 | } 349 | gl_FragColor = vec4(diffuseSum/weightSum, 1.0); 350 | }` 351 | } ); 352 | 353 | } 354 | 355 | getCompositeMaterial( nMips ) { 356 | 357 | return new ShaderMaterial( { 358 | 359 | defines: { 360 | 'NUM_MIPS': nMips 361 | }, 362 | 363 | uniforms: { 364 | 'blurTexture1': { value: null }, 365 | 'blurTexture2': { value: null }, 366 | 'blurTexture3': { value: null }, 367 | 'blurTexture4': { value: null }, 368 | 'blurTexture5': { value: null }, 369 | 'bloomStrength': { value: 1.0 }, 370 | 'bloomFactors': { value: null }, 371 | 'bloomTintColors': { value: null }, 372 | 'bloomRadius': { value: 0.0 } 373 | }, 374 | 375 | vertexShader: 376 | `varying vec2 vUv; 377 | void main() { 378 | vUv = uv; 379 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 380 | }`, 381 | 382 | fragmentShader: 383 | `varying vec2 vUv; 384 | uniform sampler2D blurTexture1; 385 | uniform sampler2D blurTexture2; 386 | uniform sampler2D blurTexture3; 387 | uniform sampler2D blurTexture4; 388 | uniform sampler2D blurTexture5; 389 | uniform float bloomStrength; 390 | uniform float bloomRadius; 391 | uniform float bloomFactors[NUM_MIPS]; 392 | uniform vec3 bloomTintColors[NUM_MIPS]; 393 | 394 | float lerpBloomFactor(const in float factor) { 395 | float mirrorFactor = 1.2 - factor; 396 | return mix(factor, mirrorFactor, bloomRadius); 397 | } 398 | 399 | void main() { 400 | gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) + 401 | lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) + 402 | lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) + 403 | lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) + 404 | lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) ); 405 | }` 406 | } ); 407 | 408 | } 409 | 410 | } 411 | 412 | UnrealBloomPass.BlurDirectionX = new Vector2( 1.0, 0.0 ); 413 | UnrealBloomPass.BlurDirectionY = new Vector2( 0.0, 1.0 ); 414 | 415 | export default UnrealBloomPass; 416 | -------------------------------------------------------------------------------- /thirdparty/OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3, 9 | Plane, 10 | Ray, 11 | MathUtils 12 | } from 'three'; 13 | 14 | // OrbitControls performs orbiting, dollying (zooming), and panning. 15 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 16 | // 17 | // Orbit - left mouse / touch: one-finger move 18 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 19 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 20 | 21 | const _changeEvent = { type: 'change' }; 22 | const _startEvent = { type: 'start' }; 23 | const _endEvent = { type: 'end' }; 24 | const _ray = new Ray(); 25 | const _plane = new Plane(); 26 | const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD ); 27 | 28 | class OrbitControls extends EventDispatcher { 29 | 30 | constructor( object, domElement ) { 31 | 32 | super(); 33 | 34 | this.object = object; 35 | this.domElement = domElement; 36 | this.domElement.style.touchAction = 'none'; // disable touch scroll 37 | 38 | // Set to false to disable this control 39 | this.enabled = true; 40 | 41 | // "target" sets the location of focus, where the object orbits around 42 | this.target = new Vector3(); 43 | 44 | // How far you can dolly in and out ( PerspectiveCamera only ) 45 | this.minDistance = 0; 46 | this.maxDistance = Infinity; 47 | 48 | // How far you can zoom in and out ( OrthographicCamera only ) 49 | this.minZoom = 0; 50 | this.maxZoom = Infinity; 51 | 52 | // How far you can orbit vertically, upper and lower limits. 53 | // Range is 0 to Math.PI radians. 54 | this.minPolarAngle = 0; // radians 55 | this.maxPolarAngle = Math.PI; // radians 56 | 57 | // How far you can orbit horizontally, upper and lower limits. 58 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 59 | this.minAzimuthAngle = - Infinity; // radians 60 | this.maxAzimuthAngle = Infinity; // radians 61 | 62 | // Set to true to enable damping (inertia) 63 | // If damping is enabled, you must call controls.update() in your animation loop 64 | this.enableDamping = false; 65 | this.dampingFactor = 0.05; 66 | 67 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 68 | // Set to false to disable zooming 69 | this.enableZoom = true; 70 | this.zoomSpeed = 1.0; 71 | 72 | // Set to false to disable rotating 73 | this.enableRotate = true; 74 | this.rotateSpeed = 1.0; 75 | 76 | // Set to false to disable panning 77 | this.enablePan = true; 78 | this.panSpeed = 1.0; 79 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 80 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 81 | this.zoomToCursor = false; 82 | 83 | // Set to true to automatically rotate around the target 84 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 85 | this.autoRotate = false; 86 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 87 | 88 | // The four arrow keys 89 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 90 | 91 | // Mouse buttons 92 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 93 | 94 | // Touch fingers 95 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 96 | 97 | // for reset 98 | this.target0 = this.target.clone(); 99 | this.position0 = this.object.position.clone(); 100 | this.zoom0 = this.object.zoom; 101 | 102 | // the target DOM element for key events 103 | this._domElementKeyEvents = null; 104 | 105 | // 106 | // public methods 107 | // 108 | 109 | this.getPolarAngle = function () { 110 | 111 | return spherical.phi; 112 | 113 | }; 114 | 115 | this.getAzimuthalAngle = function () { 116 | 117 | return spherical.theta; 118 | 119 | }; 120 | 121 | this.getDistance = function () { 122 | 123 | return this.object.position.distanceTo( this.target ); 124 | 125 | }; 126 | 127 | this.listenToKeyEvents = function ( domElement ) { 128 | 129 | domElement.addEventListener( 'keydown', onKeyDown ); 130 | this._domElementKeyEvents = domElement; 131 | 132 | }; 133 | 134 | this.stopListenToKeyEvents = function () { 135 | 136 | this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 137 | this._domElementKeyEvents = null; 138 | 139 | }; 140 | 141 | this.saveState = function () { 142 | 143 | scope.target0.copy( scope.target ); 144 | scope.position0.copy( scope.object.position ); 145 | scope.zoom0 = scope.object.zoom; 146 | 147 | }; 148 | 149 | this.reset = function () { 150 | 151 | scope.target.copy( scope.target0 ); 152 | scope.object.position.copy( scope.position0 ); 153 | scope.object.zoom = scope.zoom0; 154 | 155 | scope.object.updateProjectionMatrix(); 156 | scope.dispatchEvent( _changeEvent ); 157 | 158 | scope.update(); 159 | 160 | state = STATE.NONE; 161 | 162 | }; 163 | 164 | // this method is exposed, but perhaps it would be better if we can make it private... 165 | this.update = function () { 166 | 167 | const offset = new Vector3(); 168 | 169 | // so camera.up is the orbit axis 170 | const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 171 | const quatInverse = quat.clone().invert(); 172 | 173 | const lastPosition = new Vector3(); 174 | const lastQuaternion = new Quaternion(); 175 | const lastTargetPosition = new Vector3(); 176 | 177 | const twoPI = 2 * Math.PI; 178 | 179 | return function update( deltaTime = null ) { 180 | 181 | const position = scope.object.position; 182 | 183 | offset.copy( position ).sub( scope.target ); 184 | 185 | // rotate offset to "y-axis-is-up" space 186 | offset.applyQuaternion( quat ); 187 | 188 | // angle from z-axis around y-axis 189 | spherical.setFromVector3( offset ); 190 | 191 | if ( scope.autoRotate && state === STATE.NONE ) { 192 | 193 | rotateLeft( getAutoRotationAngle( deltaTime ) ); 194 | 195 | } 196 | 197 | if ( scope.enableDamping ) { 198 | 199 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 200 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 201 | 202 | } else { 203 | 204 | spherical.theta += sphericalDelta.theta; 205 | spherical.phi += sphericalDelta.phi; 206 | 207 | } 208 | 209 | // restrict theta to be between desired limits 210 | 211 | let min = scope.minAzimuthAngle; 212 | let max = scope.maxAzimuthAngle; 213 | 214 | if ( isFinite( min ) && isFinite( max ) ) { 215 | 216 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 217 | 218 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 219 | 220 | if ( min <= max ) { 221 | 222 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 223 | 224 | } else { 225 | 226 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? 227 | Math.max( min, spherical.theta ) : 228 | Math.min( max, spherical.theta ); 229 | 230 | } 231 | 232 | } 233 | 234 | // restrict phi to be between desired limits 235 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 236 | 237 | spherical.makeSafe(); 238 | 239 | 240 | // move target to panned location 241 | 242 | if ( scope.enableDamping === true ) { 243 | 244 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 245 | 246 | } else { 247 | 248 | scope.target.add( panOffset ); 249 | 250 | } 251 | 252 | // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera 253 | // we adjust zoom later in these cases 254 | if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) { 255 | 256 | spherical.radius = clampDistance( spherical.radius ); 257 | 258 | } else { 259 | 260 | spherical.radius = clampDistance( spherical.radius * scale ); 261 | 262 | } 263 | 264 | 265 | offset.setFromSpherical( spherical ); 266 | 267 | // rotate offset back to "camera-up-vector-is-up" space 268 | offset.applyQuaternion( quatInverse ); 269 | 270 | position.copy( scope.target ).add( offset ); 271 | 272 | scope.object.lookAt( scope.target ); 273 | 274 | if ( scope.enableDamping === true ) { 275 | 276 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 277 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 278 | 279 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 280 | 281 | } else { 282 | 283 | sphericalDelta.set( 0, 0, 0 ); 284 | 285 | panOffset.set( 0, 0, 0 ); 286 | 287 | } 288 | 289 | // adjust camera position 290 | let zoomChanged = false; 291 | if ( scope.zoomToCursor && performCursorZoom ) { 292 | 293 | let newRadius = null; 294 | if ( scope.object.isPerspectiveCamera ) { 295 | 296 | // move the camera down the pointer ray 297 | // this method avoids floating point error 298 | const prevRadius = offset.length(); 299 | newRadius = clampDistance( prevRadius * scale ); 300 | 301 | const radiusDelta = prevRadius - newRadius; 302 | scope.object.position.addScaledVector( dollyDirection, radiusDelta ); 303 | scope.object.updateMatrixWorld(); 304 | 305 | } else if ( scope.object.isOrthographicCamera ) { 306 | 307 | // adjust the ortho camera position based on zoom changes 308 | const mouseBefore = new Vector3( mouse.x, mouse.y, 0 ); 309 | mouseBefore.unproject( scope.object ); 310 | 311 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); 312 | scope.object.updateProjectionMatrix(); 313 | zoomChanged = true; 314 | 315 | const mouseAfter = new Vector3( mouse.x, mouse.y, 0 ); 316 | mouseAfter.unproject( scope.object ); 317 | 318 | scope.object.position.sub( mouseAfter ).add( mouseBefore ); 319 | scope.object.updateMatrixWorld(); 320 | 321 | newRadius = offset.length(); 322 | 323 | } else { 324 | 325 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' ); 326 | scope.zoomToCursor = false; 327 | 328 | } 329 | 330 | // handle the placement of the target 331 | if ( newRadius !== null ) { 332 | 333 | if ( this.screenSpacePanning ) { 334 | 335 | // position the orbit target in front of the new camera position 336 | scope.target.set( 0, 0, - 1 ) 337 | .transformDirection( scope.object.matrix ) 338 | .multiplyScalar( newRadius ) 339 | .add( scope.object.position ); 340 | 341 | } else { 342 | 343 | // get the ray and translation plane to compute target 344 | _ray.origin.copy( scope.object.position ); 345 | _ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix ); 346 | 347 | // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid 348 | // extremely large values 349 | if ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) { 350 | 351 | object.lookAt( scope.target ); 352 | 353 | } else { 354 | 355 | _plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target ); 356 | _ray.intersectPlane( _plane, scope.target ); 357 | 358 | } 359 | 360 | } 361 | 362 | } 363 | 364 | } else if ( scope.object.isOrthographicCamera ) { 365 | 366 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); 367 | scope.object.updateProjectionMatrix(); 368 | zoomChanged = true; 369 | 370 | } 371 | 372 | scale = 1; 373 | performCursorZoom = false; 374 | 375 | // update condition is: 376 | // min(camera displacement, camera rotation in radians)^2 > EPS 377 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 378 | 379 | if ( zoomChanged || 380 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 381 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS || 382 | lastTargetPosition.distanceToSquared( scope.target ) > 0 ) { 383 | 384 | scope.dispatchEvent( _changeEvent ); 385 | 386 | lastPosition.copy( scope.object.position ); 387 | lastQuaternion.copy( scope.object.quaternion ); 388 | lastTargetPosition.copy( scope.target ); 389 | 390 | zoomChanged = false; 391 | 392 | return true; 393 | 394 | } 395 | 396 | return false; 397 | 398 | }; 399 | 400 | }(); 401 | 402 | this.dispose = function () { 403 | 404 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 405 | 406 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 407 | scope.domElement.removeEventListener( 'pointercancel', onPointerUp ); 408 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 409 | 410 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 411 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 412 | 413 | 414 | if ( scope._domElementKeyEvents !== null ) { 415 | 416 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 417 | scope._domElementKeyEvents = null; 418 | 419 | } 420 | 421 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 422 | 423 | }; 424 | 425 | // 426 | // internals 427 | // 428 | 429 | const scope = this; 430 | 431 | const STATE = { 432 | NONE: - 1, 433 | ROTATE: 0, 434 | DOLLY: 1, 435 | PAN: 2, 436 | TOUCH_ROTATE: 3, 437 | TOUCH_PAN: 4, 438 | TOUCH_DOLLY_PAN: 5, 439 | TOUCH_DOLLY_ROTATE: 6 440 | }; 441 | 442 | let state = STATE.NONE; 443 | 444 | const EPS = 0.000001; 445 | 446 | // current position in spherical coordinates 447 | const spherical = new Spherical(); 448 | const sphericalDelta = new Spherical(); 449 | 450 | let scale = 1; 451 | const panOffset = new Vector3(); 452 | 453 | const rotateStart = new Vector2(); 454 | const rotateEnd = new Vector2(); 455 | const rotateDelta = new Vector2(); 456 | 457 | const panStart = new Vector2(); 458 | const panEnd = new Vector2(); 459 | const panDelta = new Vector2(); 460 | 461 | const dollyStart = new Vector2(); 462 | const dollyEnd = new Vector2(); 463 | const dollyDelta = new Vector2(); 464 | 465 | const dollyDirection = new Vector3(); 466 | const mouse = new Vector2(); 467 | let performCursorZoom = false; 468 | 469 | const pointers = []; 470 | const pointerPositions = {}; 471 | 472 | function getAutoRotationAngle( deltaTime ) { 473 | 474 | if ( deltaTime !== null ) { 475 | 476 | return ( 2 * Math.PI / 60 * scope.autoRotateSpeed ) * deltaTime; 477 | 478 | } else { 479 | 480 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 481 | 482 | } 483 | 484 | } 485 | 486 | function getZoomScale() { 487 | 488 | return Math.pow( 0.95, scope.zoomSpeed ); 489 | 490 | } 491 | 492 | function rotateLeft( angle ) { 493 | 494 | sphericalDelta.theta -= angle; 495 | 496 | } 497 | 498 | function rotateUp( angle ) { 499 | 500 | sphericalDelta.phi -= angle; 501 | 502 | } 503 | 504 | const panLeft = function () { 505 | 506 | const v = new Vector3(); 507 | 508 | return function panLeft( distance, objectMatrix ) { 509 | 510 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 511 | v.multiplyScalar( - distance ); 512 | 513 | panOffset.add( v ); 514 | 515 | }; 516 | 517 | }(); 518 | 519 | const panUp = function () { 520 | 521 | const v = new Vector3(); 522 | 523 | return function panUp( distance, objectMatrix ) { 524 | 525 | if ( scope.screenSpacePanning === true ) { 526 | 527 | v.setFromMatrixColumn( objectMatrix, 1 ); 528 | 529 | } else { 530 | 531 | v.setFromMatrixColumn( objectMatrix, 0 ); 532 | v.crossVectors( scope.object.up, v ); 533 | 534 | } 535 | 536 | v.multiplyScalar( distance ); 537 | 538 | panOffset.add( v ); 539 | 540 | }; 541 | 542 | }(); 543 | 544 | // deltaX and deltaY are in pixels; right and down are positive 545 | const pan = function () { 546 | 547 | const offset = new Vector3(); 548 | 549 | return function pan( deltaX, deltaY ) { 550 | 551 | const element = scope.domElement; 552 | 553 | if ( scope.object.isPerspectiveCamera ) { 554 | 555 | // perspective 556 | const position = scope.object.position; 557 | offset.copy( position ).sub( scope.target ); 558 | let targetDistance = offset.length(); 559 | 560 | // half of the fov is center to top of screen 561 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 562 | 563 | // we use only clientHeight here so aspect ratio does not distort speed 564 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 565 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 566 | 567 | } else if ( scope.object.isOrthographicCamera ) { 568 | 569 | // orthographic 570 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 571 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 572 | 573 | } else { 574 | 575 | // camera neither orthographic nor perspective 576 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 577 | scope.enablePan = false; 578 | 579 | } 580 | 581 | }; 582 | 583 | }(); 584 | 585 | function dollyOut( dollyScale ) { 586 | 587 | if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { 588 | 589 | scale /= dollyScale; 590 | 591 | } else { 592 | 593 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 594 | scope.enableZoom = false; 595 | 596 | } 597 | 598 | } 599 | 600 | function dollyIn( dollyScale ) { 601 | 602 | if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { 603 | 604 | scale *= dollyScale; 605 | 606 | } else { 607 | 608 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 609 | scope.enableZoom = false; 610 | 611 | } 612 | 613 | } 614 | 615 | function updateMouseParameters( event ) { 616 | 617 | if ( ! scope.zoomToCursor ) { 618 | 619 | return; 620 | 621 | } 622 | 623 | performCursorZoom = true; 624 | 625 | const rect = scope.domElement.getBoundingClientRect(); 626 | const x = event.clientX - rect.left; 627 | const y = event.clientY - rect.top; 628 | const w = rect.width; 629 | const h = rect.height; 630 | 631 | mouse.x = ( x / w ) * 2 - 1; 632 | mouse.y = - ( y / h ) * 2 + 1; 633 | 634 | dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( scope.object ).sub( scope.object.position ).normalize(); 635 | 636 | } 637 | 638 | function clampDistance( dist ) { 639 | 640 | return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) ); 641 | 642 | } 643 | 644 | // 645 | // event callbacks - update the object state 646 | // 647 | 648 | function handleMouseDownRotate( event ) { 649 | 650 | rotateStart.set( event.clientX, event.clientY ); 651 | 652 | } 653 | 654 | function handleMouseDownDolly( event ) { 655 | 656 | updateMouseParameters( event ); 657 | dollyStart.set( event.clientX, event.clientY ); 658 | 659 | } 660 | 661 | function handleMouseDownPan( event ) { 662 | 663 | panStart.set( event.clientX, event.clientY ); 664 | 665 | } 666 | 667 | function handleMouseMoveRotate( event ) { 668 | 669 | rotateEnd.set( event.clientX, event.clientY ); 670 | 671 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 672 | 673 | const element = scope.domElement; 674 | 675 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 676 | 677 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 678 | 679 | rotateStart.copy( rotateEnd ); 680 | 681 | scope.update(); 682 | 683 | } 684 | 685 | function handleMouseMoveDolly( event ) { 686 | 687 | dollyEnd.set( event.clientX, event.clientY ); 688 | 689 | dollyDelta.subVectors( dollyEnd, dollyStart ); 690 | 691 | if ( dollyDelta.y > 0 ) { 692 | 693 | dollyOut( getZoomScale() ); 694 | 695 | } else if ( dollyDelta.y < 0 ) { 696 | 697 | dollyIn( getZoomScale() ); 698 | 699 | } 700 | 701 | dollyStart.copy( dollyEnd ); 702 | 703 | scope.update(); 704 | 705 | } 706 | 707 | function handleMouseMovePan( event ) { 708 | 709 | panEnd.set( event.clientX, event.clientY ); 710 | 711 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 712 | 713 | pan( panDelta.x, panDelta.y ); 714 | 715 | panStart.copy( panEnd ); 716 | 717 | scope.update(); 718 | 719 | } 720 | 721 | function handleMouseWheel( event ) { 722 | 723 | updateMouseParameters( event ); 724 | 725 | if ( event.deltaY < 0 ) { 726 | 727 | dollyIn( getZoomScale() ); 728 | 729 | } else if ( event.deltaY > 0 ) { 730 | 731 | dollyOut( getZoomScale() ); 732 | 733 | } 734 | 735 | scope.update(); 736 | 737 | } 738 | 739 | function handleKeyDown( event ) { 740 | 741 | let needsUpdate = false; 742 | 743 | switch ( event.code ) { 744 | 745 | case scope.keys.UP: 746 | 747 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 748 | 749 | rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 750 | 751 | } else { 752 | 753 | pan( 0, scope.keyPanSpeed ); 754 | 755 | } 756 | 757 | needsUpdate = true; 758 | break; 759 | 760 | case scope.keys.BOTTOM: 761 | 762 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 763 | 764 | rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 765 | 766 | } else { 767 | 768 | pan( 0, - scope.keyPanSpeed ); 769 | 770 | } 771 | 772 | needsUpdate = true; 773 | break; 774 | 775 | case scope.keys.LEFT: 776 | 777 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 778 | 779 | rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 780 | 781 | } else { 782 | 783 | pan( scope.keyPanSpeed, 0 ); 784 | 785 | } 786 | 787 | needsUpdate = true; 788 | break; 789 | 790 | case scope.keys.RIGHT: 791 | 792 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 793 | 794 | rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 795 | 796 | } else { 797 | 798 | pan( - scope.keyPanSpeed, 0 ); 799 | 800 | } 801 | 802 | needsUpdate = true; 803 | break; 804 | 805 | } 806 | 807 | if ( needsUpdate ) { 808 | 809 | // prevent the browser from scrolling on cursor keys 810 | event.preventDefault(); 811 | 812 | scope.update(); 813 | 814 | } 815 | 816 | 817 | } 818 | 819 | function handleTouchStartRotate() { 820 | 821 | if ( pointers.length === 1 ) { 822 | 823 | rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 824 | 825 | } else { 826 | 827 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 828 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 829 | 830 | rotateStart.set( x, y ); 831 | 832 | } 833 | 834 | } 835 | 836 | function handleTouchStartPan() { 837 | 838 | if ( pointers.length === 1 ) { 839 | 840 | panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 841 | 842 | } else { 843 | 844 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 845 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 846 | 847 | panStart.set( x, y ); 848 | 849 | } 850 | 851 | } 852 | 853 | function handleTouchStartDolly() { 854 | 855 | const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; 856 | const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; 857 | 858 | const distance = Math.sqrt( dx * dx + dy * dy ); 859 | 860 | dollyStart.set( 0, distance ); 861 | 862 | } 863 | 864 | function handleTouchStartDollyPan() { 865 | 866 | if ( scope.enableZoom ) handleTouchStartDolly(); 867 | 868 | if ( scope.enablePan ) handleTouchStartPan(); 869 | 870 | } 871 | 872 | function handleTouchStartDollyRotate() { 873 | 874 | if ( scope.enableZoom ) handleTouchStartDolly(); 875 | 876 | if ( scope.enableRotate ) handleTouchStartRotate(); 877 | 878 | } 879 | 880 | function handleTouchMoveRotate( event ) { 881 | 882 | if ( pointers.length == 1 ) { 883 | 884 | rotateEnd.set( event.pageX, event.pageY ); 885 | 886 | } else { 887 | 888 | const position = getSecondPointerPosition( event ); 889 | 890 | const x = 0.5 * ( event.pageX + position.x ); 891 | const y = 0.5 * ( event.pageY + position.y ); 892 | 893 | rotateEnd.set( x, y ); 894 | 895 | } 896 | 897 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 898 | 899 | const element = scope.domElement; 900 | 901 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 902 | 903 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 904 | 905 | rotateStart.copy( rotateEnd ); 906 | 907 | } 908 | 909 | function handleTouchMovePan( event ) { 910 | 911 | if ( pointers.length === 1 ) { 912 | 913 | panEnd.set( event.pageX, event.pageY ); 914 | 915 | } else { 916 | 917 | const position = getSecondPointerPosition( event ); 918 | 919 | const x = 0.5 * ( event.pageX + position.x ); 920 | const y = 0.5 * ( event.pageY + position.y ); 921 | 922 | panEnd.set( x, y ); 923 | 924 | } 925 | 926 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 927 | 928 | pan( panDelta.x, panDelta.y ); 929 | 930 | panStart.copy( panEnd ); 931 | 932 | } 933 | 934 | function handleTouchMoveDolly( event ) { 935 | 936 | const position = getSecondPointerPosition( event ); 937 | 938 | const dx = event.pageX - position.x; 939 | const dy = event.pageY - position.y; 940 | 941 | const distance = Math.sqrt( dx * dx + dy * dy ); 942 | 943 | dollyEnd.set( 0, distance ); 944 | 945 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 946 | 947 | dollyOut( dollyDelta.y ); 948 | 949 | dollyStart.copy( dollyEnd ); 950 | 951 | } 952 | 953 | function handleTouchMoveDollyPan( event ) { 954 | 955 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 956 | 957 | if ( scope.enablePan ) handleTouchMovePan( event ); 958 | 959 | } 960 | 961 | function handleTouchMoveDollyRotate( event ) { 962 | 963 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 964 | 965 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 966 | 967 | } 968 | 969 | // 970 | // event handlers - FSM: listen for events and reset state 971 | // 972 | 973 | function onPointerDown( event ) { 974 | 975 | if ( scope.enabled === false ) return; 976 | 977 | if ( pointers.length === 0 ) { 978 | 979 | scope.domElement.setPointerCapture( event.pointerId ); 980 | 981 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 982 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 983 | 984 | } 985 | 986 | // 987 | 988 | addPointer( event ); 989 | 990 | if ( event.pointerType === 'touch' ) { 991 | 992 | onTouchStart( event ); 993 | 994 | } else { 995 | 996 | onMouseDown( event ); 997 | 998 | } 999 | 1000 | } 1001 | 1002 | function onPointerMove( event ) { 1003 | 1004 | if ( scope.enabled === false ) return; 1005 | 1006 | if ( event.pointerType === 'touch' ) { 1007 | 1008 | onTouchMove( event ); 1009 | 1010 | } else { 1011 | 1012 | onMouseMove( event ); 1013 | 1014 | } 1015 | 1016 | } 1017 | 1018 | function onPointerUp( event ) { 1019 | 1020 | removePointer( event ); 1021 | 1022 | if ( pointers.length === 0 ) { 1023 | 1024 | scope.domElement.releasePointerCapture( event.pointerId ); 1025 | 1026 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 1027 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 1028 | 1029 | } 1030 | 1031 | scope.dispatchEvent( _endEvent ); 1032 | 1033 | state = STATE.NONE; 1034 | 1035 | } 1036 | 1037 | function onMouseDown( event ) { 1038 | 1039 | let mouseAction; 1040 | 1041 | switch ( event.button ) { 1042 | 1043 | case 0: 1044 | 1045 | mouseAction = scope.mouseButtons.LEFT; 1046 | break; 1047 | 1048 | case 1: 1049 | 1050 | mouseAction = scope.mouseButtons.MIDDLE; 1051 | break; 1052 | 1053 | case 2: 1054 | 1055 | mouseAction = scope.mouseButtons.RIGHT; 1056 | break; 1057 | 1058 | default: 1059 | 1060 | mouseAction = - 1; 1061 | 1062 | } 1063 | 1064 | switch ( mouseAction ) { 1065 | 1066 | case MOUSE.DOLLY: 1067 | 1068 | if ( scope.enableZoom === false ) return; 1069 | 1070 | handleMouseDownDolly( event ); 1071 | 1072 | state = STATE.DOLLY; 1073 | 1074 | break; 1075 | 1076 | case MOUSE.ROTATE: 1077 | 1078 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 1079 | 1080 | if ( scope.enablePan === false ) return; 1081 | 1082 | handleMouseDownPan( event ); 1083 | 1084 | state = STATE.PAN; 1085 | 1086 | } else { 1087 | 1088 | if ( scope.enableRotate === false ) return; 1089 | 1090 | handleMouseDownRotate( event ); 1091 | 1092 | state = STATE.ROTATE; 1093 | 1094 | } 1095 | 1096 | break; 1097 | 1098 | case MOUSE.PAN: 1099 | 1100 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 1101 | 1102 | if ( scope.enableRotate === false ) return; 1103 | 1104 | handleMouseDownRotate( event ); 1105 | 1106 | state = STATE.ROTATE; 1107 | 1108 | } else { 1109 | 1110 | if ( scope.enablePan === false ) return; 1111 | 1112 | handleMouseDownPan( event ); 1113 | 1114 | state = STATE.PAN; 1115 | 1116 | } 1117 | 1118 | break; 1119 | 1120 | default: 1121 | 1122 | state = STATE.NONE; 1123 | 1124 | } 1125 | 1126 | if ( state !== STATE.NONE ) { 1127 | 1128 | scope.dispatchEvent( _startEvent ); 1129 | 1130 | } 1131 | 1132 | } 1133 | 1134 | function onMouseMove( event ) { 1135 | 1136 | switch ( state ) { 1137 | 1138 | case STATE.ROTATE: 1139 | 1140 | if ( scope.enableRotate === false ) return; 1141 | 1142 | handleMouseMoveRotate( event ); 1143 | 1144 | break; 1145 | 1146 | case STATE.DOLLY: 1147 | 1148 | if ( scope.enableZoom === false ) return; 1149 | 1150 | handleMouseMoveDolly( event ); 1151 | 1152 | break; 1153 | 1154 | case STATE.PAN: 1155 | 1156 | if ( scope.enablePan === false ) return; 1157 | 1158 | handleMouseMovePan( event ); 1159 | 1160 | break; 1161 | 1162 | } 1163 | 1164 | } 1165 | 1166 | function onMouseWheel( event ) { 1167 | 1168 | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; 1169 | 1170 | event.preventDefault(); 1171 | 1172 | scope.dispatchEvent( _startEvent ); 1173 | 1174 | handleMouseWheel( event ); 1175 | 1176 | scope.dispatchEvent( _endEvent ); 1177 | 1178 | } 1179 | 1180 | function onKeyDown( event ) { 1181 | 1182 | if ( scope.enabled === false || scope.enablePan === false ) return; 1183 | 1184 | handleKeyDown( event ); 1185 | 1186 | } 1187 | 1188 | function onTouchStart( event ) { 1189 | 1190 | trackPointer( event ); 1191 | 1192 | switch ( pointers.length ) { 1193 | 1194 | case 1: 1195 | 1196 | switch ( scope.touches.ONE ) { 1197 | 1198 | case TOUCH.ROTATE: 1199 | 1200 | if ( scope.enableRotate === false ) return; 1201 | 1202 | handleTouchStartRotate(); 1203 | 1204 | state = STATE.TOUCH_ROTATE; 1205 | 1206 | break; 1207 | 1208 | case TOUCH.PAN: 1209 | 1210 | if ( scope.enablePan === false ) return; 1211 | 1212 | handleTouchStartPan(); 1213 | 1214 | state = STATE.TOUCH_PAN; 1215 | 1216 | break; 1217 | 1218 | default: 1219 | 1220 | state = STATE.NONE; 1221 | 1222 | } 1223 | 1224 | break; 1225 | 1226 | case 2: 1227 | 1228 | switch ( scope.touches.TWO ) { 1229 | 1230 | case TOUCH.DOLLY_PAN: 1231 | 1232 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1233 | 1234 | handleTouchStartDollyPan(); 1235 | 1236 | state = STATE.TOUCH_DOLLY_PAN; 1237 | 1238 | break; 1239 | 1240 | case TOUCH.DOLLY_ROTATE: 1241 | 1242 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1243 | 1244 | handleTouchStartDollyRotate(); 1245 | 1246 | state = STATE.TOUCH_DOLLY_ROTATE; 1247 | 1248 | break; 1249 | 1250 | default: 1251 | 1252 | state = STATE.NONE; 1253 | 1254 | } 1255 | 1256 | break; 1257 | 1258 | default: 1259 | 1260 | state = STATE.NONE; 1261 | 1262 | } 1263 | 1264 | if ( state !== STATE.NONE ) { 1265 | 1266 | scope.dispatchEvent( _startEvent ); 1267 | 1268 | } 1269 | 1270 | } 1271 | 1272 | function onTouchMove( event ) { 1273 | 1274 | trackPointer( event ); 1275 | 1276 | switch ( state ) { 1277 | 1278 | case STATE.TOUCH_ROTATE: 1279 | 1280 | if ( scope.enableRotate === false ) return; 1281 | 1282 | handleTouchMoveRotate( event ); 1283 | 1284 | scope.update(); 1285 | 1286 | break; 1287 | 1288 | case STATE.TOUCH_PAN: 1289 | 1290 | if ( scope.enablePan === false ) return; 1291 | 1292 | handleTouchMovePan( event ); 1293 | 1294 | scope.update(); 1295 | 1296 | break; 1297 | 1298 | case STATE.TOUCH_DOLLY_PAN: 1299 | 1300 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1301 | 1302 | handleTouchMoveDollyPan( event ); 1303 | 1304 | scope.update(); 1305 | 1306 | break; 1307 | 1308 | case STATE.TOUCH_DOLLY_ROTATE: 1309 | 1310 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1311 | 1312 | handleTouchMoveDollyRotate( event ); 1313 | 1314 | scope.update(); 1315 | 1316 | break; 1317 | 1318 | default: 1319 | 1320 | state = STATE.NONE; 1321 | 1322 | } 1323 | 1324 | } 1325 | 1326 | function onContextMenu( event ) { 1327 | 1328 | if ( scope.enabled === false ) return; 1329 | 1330 | event.preventDefault(); 1331 | 1332 | } 1333 | 1334 | function addPointer( event ) { 1335 | 1336 | pointers.push( event ); 1337 | 1338 | } 1339 | 1340 | function removePointer( event ) { 1341 | 1342 | delete pointerPositions[ event.pointerId ]; 1343 | 1344 | for ( let i = 0; i < pointers.length; i ++ ) { 1345 | 1346 | if ( pointers[ i ].pointerId == event.pointerId ) { 1347 | 1348 | pointers.splice( i, 1 ); 1349 | return; 1350 | 1351 | } 1352 | 1353 | } 1354 | 1355 | } 1356 | 1357 | function trackPointer( event ) { 1358 | 1359 | let position = pointerPositions[ event.pointerId ]; 1360 | 1361 | if ( position === undefined ) { 1362 | 1363 | position = new Vector2(); 1364 | pointerPositions[ event.pointerId ] = position; 1365 | 1366 | } 1367 | 1368 | position.set( event.pageX, event.pageY ); 1369 | 1370 | } 1371 | 1372 | function getSecondPointerPosition( event ) { 1373 | 1374 | const pointer = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ] : pointers[ 0 ]; 1375 | 1376 | return pointerPositions[ pointer.pointerId ]; 1377 | 1378 | } 1379 | 1380 | // 1381 | 1382 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1383 | 1384 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1385 | scope.domElement.addEventListener( 'pointercancel', onPointerUp ); 1386 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 1387 | 1388 | // force an update at start 1389 | 1390 | this.update(); 1391 | 1392 | } 1393 | 1394 | } 1395 | 1396 | export { OrbitControls }; 1397 | -------------------------------------------------------------------------------- /thirdparty/BufferGeometryUtils.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Float32BufferAttribute, 5 | InstancedBufferAttribute, 6 | InterleavedBuffer, 7 | InterleavedBufferAttribute, 8 | TriangleFanDrawMode, 9 | TriangleStripDrawMode, 10 | TrianglesDrawMode, 11 | Vector3, 12 | } from 'three'; 13 | 14 | function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) { 15 | 16 | if ( ! MikkTSpace || ! MikkTSpace.isReady ) { 17 | 18 | throw new Error( 'BufferGeometryUtils: Initialized MikkTSpace library required.' ); 19 | 20 | } 21 | 22 | if ( ! geometry.hasAttribute( 'position' ) || ! geometry.hasAttribute( 'normal' ) || ! geometry.hasAttribute( 'uv' ) ) { 23 | 24 | throw new Error( 'BufferGeometryUtils: Tangents require "position", "normal", and "uv" attributes.' ); 25 | 26 | } 27 | 28 | function getAttributeArray( attribute ) { 29 | 30 | if ( attribute.normalized || attribute.isInterleavedBufferAttribute ) { 31 | 32 | const dstArray = new Float32Array( attribute.count * attribute.itemSize ); 33 | 34 | for ( let i = 0, j = 0; i < attribute.count; i ++ ) { 35 | 36 | dstArray[ j ++ ] = attribute.getX( i ); 37 | dstArray[ j ++ ] = attribute.getY( i ); 38 | 39 | if ( attribute.itemSize > 2 ) { 40 | 41 | dstArray[ j ++ ] = attribute.getZ( i ); 42 | 43 | } 44 | 45 | } 46 | 47 | return dstArray; 48 | 49 | } 50 | 51 | if ( attribute.array instanceof Float32Array ) { 52 | 53 | return attribute.array; 54 | 55 | } 56 | 57 | return new Float32Array( attribute.array ); 58 | 59 | } 60 | 61 | // MikkTSpace algorithm requires non-indexed input. 62 | 63 | const _geometry = geometry.index ? geometry.toNonIndexed() : geometry; 64 | 65 | // Compute vertex tangents. 66 | 67 | const tangents = MikkTSpace.generateTangents( 68 | 69 | getAttributeArray( _geometry.attributes.position ), 70 | getAttributeArray( _geometry.attributes.normal ), 71 | getAttributeArray( _geometry.attributes.uv ) 72 | 73 | ); 74 | 75 | // Texture coordinate convention of glTF differs from the apparent 76 | // default of the MikkTSpace library; .w component must be flipped. 77 | 78 | if ( negateSign ) { 79 | 80 | for ( let i = 3; i < tangents.length; i += 4 ) { 81 | 82 | tangents[ i ] *= - 1; 83 | 84 | } 85 | 86 | } 87 | 88 | // 89 | 90 | _geometry.setAttribute( 'tangent', new BufferAttribute( tangents, 4 ) ); 91 | 92 | if ( geometry !== _geometry ) { 93 | 94 | geometry.copy( _geometry ); 95 | 96 | } 97 | 98 | return geometry; 99 | 100 | } 101 | 102 | /** 103 | * @param {Array} geometries 104 | * @param {Boolean} useGroups 105 | * @return {BufferGeometry} 106 | */ 107 | function mergeGeometries( geometries, useGroups = false ) { 108 | 109 | const isIndexed = geometries[ 0 ].index !== null; 110 | 111 | const attributesUsed = new Set( Object.keys( geometries[ 0 ].attributes ) ); 112 | const morphAttributesUsed = new Set( Object.keys( geometries[ 0 ].morphAttributes ) ); 113 | 114 | const attributes = {}; 115 | const morphAttributes = {}; 116 | 117 | const morphTargetsRelative = geometries[ 0 ].morphTargetsRelative; 118 | 119 | const mergedGeometry = new BufferGeometry(); 120 | 121 | let offset = 0; 122 | 123 | for ( let i = 0; i < geometries.length; ++ i ) { 124 | 125 | const geometry = geometries[ i ]; 126 | let attributesCount = 0; 127 | 128 | // ensure that all geometries are indexed, or none 129 | 130 | if ( isIndexed !== ( geometry.index !== null ) ) { 131 | 132 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.' ); 133 | return null; 134 | 135 | } 136 | 137 | // gather attributes, exit early if they're different 138 | 139 | for ( const name in geometry.attributes ) { 140 | 141 | if ( ! attributesUsed.has( name ) ) { 142 | 143 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.' ); 144 | return null; 145 | 146 | } 147 | 148 | if ( attributes[ name ] === undefined ) attributes[ name ] = []; 149 | 150 | attributes[ name ].push( geometry.attributes[ name ] ); 151 | 152 | attributesCount ++; 153 | 154 | } 155 | 156 | // ensure geometries have the same number of attributes 157 | 158 | if ( attributesCount !== attributesUsed.size ) { 159 | 160 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. Make sure all geometries have the same number of attributes.' ); 161 | return null; 162 | 163 | } 164 | 165 | // gather morph attributes, exit early if they're different 166 | 167 | if ( morphTargetsRelative !== geometry.morphTargetsRelative ) { 168 | 169 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphTargetsRelative must be consistent throughout all geometries.' ); 170 | return null; 171 | 172 | } 173 | 174 | for ( const name in geometry.morphAttributes ) { 175 | 176 | if ( ! morphAttributesUsed.has( name ) ) { 177 | 178 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphAttributes must be consistent throughout all geometries.' ); 179 | return null; 180 | 181 | } 182 | 183 | if ( morphAttributes[ name ] === undefined ) morphAttributes[ name ] = []; 184 | 185 | morphAttributes[ name ].push( geometry.morphAttributes[ name ] ); 186 | 187 | } 188 | 189 | if ( useGroups ) { 190 | 191 | let count; 192 | 193 | if ( isIndexed ) { 194 | 195 | count = geometry.index.count; 196 | 197 | } else if ( geometry.attributes.position !== undefined ) { 198 | 199 | count = geometry.attributes.position.count; 200 | 201 | } else { 202 | 203 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. The geometry must have either an index or a position attribute' ); 204 | return null; 205 | 206 | } 207 | 208 | mergedGeometry.addGroup( offset, count, i ); 209 | 210 | offset += count; 211 | 212 | } 213 | 214 | } 215 | 216 | // merge indices 217 | 218 | if ( isIndexed ) { 219 | 220 | let indexOffset = 0; 221 | const mergedIndex = []; 222 | 223 | for ( let i = 0; i < geometries.length; ++ i ) { 224 | 225 | const index = geometries[ i ].index; 226 | 227 | for ( let j = 0; j < index.count; ++ j ) { 228 | 229 | mergedIndex.push( index.getX( j ) + indexOffset ); 230 | 231 | } 232 | 233 | indexOffset += geometries[ i ].attributes.position.count; 234 | 235 | } 236 | 237 | mergedGeometry.setIndex( mergedIndex ); 238 | 239 | } 240 | 241 | // merge attributes 242 | 243 | for ( const name in attributes ) { 244 | 245 | const mergedAttribute = mergeAttributes( attributes[ name ] ); 246 | 247 | if ( ! mergedAttribute ) { 248 | 249 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' attribute.' ); 250 | return null; 251 | 252 | } 253 | 254 | mergedGeometry.setAttribute( name, mergedAttribute ); 255 | 256 | } 257 | 258 | // merge morph attributes 259 | 260 | for ( const name in morphAttributes ) { 261 | 262 | const numMorphTargets = morphAttributes[ name ][ 0 ].length; 263 | 264 | if ( numMorphTargets === 0 ) break; 265 | 266 | mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {}; 267 | mergedGeometry.morphAttributes[ name ] = []; 268 | 269 | for ( let i = 0; i < numMorphTargets; ++ i ) { 270 | 271 | const morphAttributesToMerge = []; 272 | 273 | for ( let j = 0; j < morphAttributes[ name ].length; ++ j ) { 274 | 275 | morphAttributesToMerge.push( morphAttributes[ name ][ j ][ i ] ); 276 | 277 | } 278 | 279 | const mergedMorphAttribute = mergeAttributes( morphAttributesToMerge ); 280 | 281 | if ( ! mergedMorphAttribute ) { 282 | 283 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' morphAttribute.' ); 284 | return null; 285 | 286 | } 287 | 288 | mergedGeometry.morphAttributes[ name ].push( mergedMorphAttribute ); 289 | 290 | } 291 | 292 | } 293 | 294 | return mergedGeometry; 295 | 296 | } 297 | 298 | /** 299 | * @param {Array} attributes 300 | * @return {BufferAttribute} 301 | */ 302 | function mergeAttributes( attributes ) { 303 | 304 | let TypedArray; 305 | let itemSize; 306 | let normalized; 307 | let gpuType = - 1; 308 | let arrayLength = 0; 309 | 310 | for ( let i = 0; i < attributes.length; ++ i ) { 311 | 312 | const attribute = attributes[ i ]; 313 | 314 | if ( attribute.isInterleavedBufferAttribute ) { 315 | 316 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. InterleavedBufferAttributes are not supported.' ); 317 | return null; 318 | 319 | } 320 | 321 | if ( TypedArray === undefined ) TypedArray = attribute.array.constructor; 322 | if ( TypedArray !== attribute.array.constructor ) { 323 | 324 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.' ); 325 | return null; 326 | 327 | } 328 | 329 | if ( itemSize === undefined ) itemSize = attribute.itemSize; 330 | if ( itemSize !== attribute.itemSize ) { 331 | 332 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.' ); 333 | return null; 334 | 335 | } 336 | 337 | if ( normalized === undefined ) normalized = attribute.normalized; 338 | if ( normalized !== attribute.normalized ) { 339 | 340 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.' ); 341 | return null; 342 | 343 | } 344 | 345 | if ( gpuType === - 1 ) gpuType = attribute.gpuType; 346 | if ( gpuType !== attribute.gpuType ) { 347 | 348 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.gpuType must be consistent across matching attributes.' ); 349 | return null; 350 | 351 | } 352 | 353 | arrayLength += attribute.array.length; 354 | 355 | } 356 | 357 | const array = new TypedArray( arrayLength ); 358 | let offset = 0; 359 | 360 | for ( let i = 0; i < attributes.length; ++ i ) { 361 | 362 | array.set( attributes[ i ].array, offset ); 363 | 364 | offset += attributes[ i ].array.length; 365 | 366 | } 367 | 368 | const result = new BufferAttribute( array, itemSize, normalized ); 369 | if ( gpuType !== undefined ) { 370 | 371 | result.gpuType = gpuType; 372 | 373 | } 374 | 375 | return result; 376 | 377 | } 378 | 379 | /** 380 | * @param {BufferAttribute} 381 | * @return {BufferAttribute} 382 | */ 383 | export function deepCloneAttribute( attribute ) { 384 | 385 | if ( attribute.isInstancedInterleavedBufferAttribute || attribute.isInterleavedBufferAttribute ) { 386 | 387 | return deinterleaveAttribute( attribute ); 388 | 389 | } 390 | 391 | if ( attribute.isInstancedBufferAttribute ) { 392 | 393 | return new InstancedBufferAttribute().copy( attribute ); 394 | 395 | } 396 | 397 | return new BufferAttribute().copy( attribute ); 398 | 399 | } 400 | 401 | /** 402 | * @param {Array} attributes 403 | * @return {Array} 404 | */ 405 | function interleaveAttributes( attributes ) { 406 | 407 | // Interleaves the provided attributes into an InterleavedBuffer and returns 408 | // a set of InterleavedBufferAttributes for each attribute 409 | let TypedArray; 410 | let arrayLength = 0; 411 | let stride = 0; 412 | 413 | // calculate the length and type of the interleavedBuffer 414 | for ( let i = 0, l = attributes.length; i < l; ++ i ) { 415 | 416 | const attribute = attributes[ i ]; 417 | 418 | if ( TypedArray === undefined ) TypedArray = attribute.array.constructor; 419 | if ( TypedArray !== attribute.array.constructor ) { 420 | 421 | console.error( 'AttributeBuffers of different types cannot be interleaved' ); 422 | return null; 423 | 424 | } 425 | 426 | arrayLength += attribute.array.length; 427 | stride += attribute.itemSize; 428 | 429 | } 430 | 431 | // Create the set of buffer attributes 432 | const interleavedBuffer = new InterleavedBuffer( new TypedArray( arrayLength ), stride ); 433 | let offset = 0; 434 | const res = []; 435 | const getters = [ 'getX', 'getY', 'getZ', 'getW' ]; 436 | const setters = [ 'setX', 'setY', 'setZ', 'setW' ]; 437 | 438 | for ( let j = 0, l = attributes.length; j < l; j ++ ) { 439 | 440 | const attribute = attributes[ j ]; 441 | const itemSize = attribute.itemSize; 442 | const count = attribute.count; 443 | const iba = new InterleavedBufferAttribute( interleavedBuffer, itemSize, offset, attribute.normalized ); 444 | res.push( iba ); 445 | 446 | offset += itemSize; 447 | 448 | // Move the data for each attribute into the new interleavedBuffer 449 | // at the appropriate offset 450 | for ( let c = 0; c < count; c ++ ) { 451 | 452 | for ( let k = 0; k < itemSize; k ++ ) { 453 | 454 | iba[ setters[ k ] ]( c, attribute[ getters[ k ] ]( c ) ); 455 | 456 | } 457 | 458 | } 459 | 460 | } 461 | 462 | return res; 463 | 464 | } 465 | 466 | // returns a new, non-interleaved version of the provided attribute 467 | export function deinterleaveAttribute( attribute ) { 468 | 469 | const cons = attribute.data.array.constructor; 470 | const count = attribute.count; 471 | const itemSize = attribute.itemSize; 472 | const normalized = attribute.normalized; 473 | 474 | const array = new cons( count * itemSize ); 475 | let newAttribute; 476 | if ( attribute.isInstancedInterleavedBufferAttribute ) { 477 | 478 | newAttribute = new InstancedBufferAttribute( array, itemSize, normalized, attribute.meshPerAttribute ); 479 | 480 | } else { 481 | 482 | newAttribute = new BufferAttribute( array, itemSize, normalized ); 483 | 484 | } 485 | 486 | for ( let i = 0; i < count; i ++ ) { 487 | 488 | newAttribute.setX( i, attribute.getX( i ) ); 489 | 490 | if ( itemSize >= 2 ) { 491 | 492 | newAttribute.setY( i, attribute.getY( i ) ); 493 | 494 | } 495 | 496 | if ( itemSize >= 3 ) { 497 | 498 | newAttribute.setZ( i, attribute.getZ( i ) ); 499 | 500 | } 501 | 502 | if ( itemSize >= 4 ) { 503 | 504 | newAttribute.setW( i, attribute.getW( i ) ); 505 | 506 | } 507 | 508 | } 509 | 510 | return newAttribute; 511 | 512 | } 513 | 514 | // deinterleaves all attributes on the geometry 515 | export function deinterleaveGeometry( geometry ) { 516 | 517 | const attributes = geometry.attributes; 518 | const morphTargets = geometry.morphTargets; 519 | const attrMap = new Map(); 520 | 521 | for ( const key in attributes ) { 522 | 523 | const attr = attributes[ key ]; 524 | if ( attr.isInterleavedBufferAttribute ) { 525 | 526 | if ( ! attrMap.has( attr ) ) { 527 | 528 | attrMap.set( attr, deinterleaveAttribute( attr ) ); 529 | 530 | } 531 | 532 | attributes[ key ] = attrMap.get( attr ); 533 | 534 | } 535 | 536 | } 537 | 538 | for ( const key in morphTargets ) { 539 | 540 | const attr = morphTargets[ key ]; 541 | if ( attr.isInterleavedBufferAttribute ) { 542 | 543 | if ( ! attrMap.has( attr ) ) { 544 | 545 | attrMap.set( attr, deinterleaveAttribute( attr ) ); 546 | 547 | } 548 | 549 | morphTargets[ key ] = attrMap.get( attr ); 550 | 551 | } 552 | 553 | } 554 | 555 | } 556 | 557 | /** 558 | * @param {BufferGeometry} geometry 559 | * @return {number} 560 | */ 561 | function estimateBytesUsed( geometry ) { 562 | 563 | // Return the estimated memory used by this geometry in bytes 564 | // Calculate using itemSize, count, and BYTES_PER_ELEMENT to account 565 | // for InterleavedBufferAttributes. 566 | let mem = 0; 567 | for ( const name in geometry.attributes ) { 568 | 569 | const attr = geometry.getAttribute( name ); 570 | mem += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT; 571 | 572 | } 573 | 574 | const indices = geometry.getIndex(); 575 | mem += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0; 576 | return mem; 577 | 578 | } 579 | 580 | /** 581 | * @param {BufferGeometry} geometry 582 | * @param {number} tolerance 583 | * @return {BufferGeometry} 584 | */ 585 | function mergeVertices( geometry, tolerance = 1e-4 ) { 586 | 587 | tolerance = Math.max( tolerance, Number.EPSILON ); 588 | 589 | // Generate an index buffer if the geometry doesn't have one, or optimize it 590 | // if it's already available. 591 | const hashToIndex = {}; 592 | const indices = geometry.getIndex(); 593 | const positions = geometry.getAttribute( 'position' ); 594 | const vertexCount = indices ? indices.count : positions.count; 595 | 596 | // next value for triangle indices 597 | let nextIndex = 0; 598 | 599 | // attributes and new attribute arrays 600 | const attributeNames = Object.keys( geometry.attributes ); 601 | const tmpAttributes = {}; 602 | const tmpMorphAttributes = {}; 603 | const newIndices = []; 604 | const getters = [ 'getX', 'getY', 'getZ', 'getW' ]; 605 | const setters = [ 'setX', 'setY', 'setZ', 'setW' ]; 606 | 607 | // Initialize the arrays, allocating space conservatively. Extra 608 | // space will be trimmed in the last step. 609 | for ( let i = 0, l = attributeNames.length; i < l; i ++ ) { 610 | 611 | const name = attributeNames[ i ]; 612 | const attr = geometry.attributes[ name ]; 613 | 614 | tmpAttributes[ name ] = new BufferAttribute( 615 | new attr.array.constructor( attr.count * attr.itemSize ), 616 | attr.itemSize, 617 | attr.normalized 618 | ); 619 | 620 | const morphAttr = geometry.morphAttributes[ name ]; 621 | if ( morphAttr ) { 622 | 623 | tmpMorphAttributes[ name ] = new BufferAttribute( 624 | new morphAttr.array.constructor( morphAttr.count * morphAttr.itemSize ), 625 | morphAttr.itemSize, 626 | morphAttr.normalized 627 | ); 628 | 629 | } 630 | 631 | } 632 | 633 | // convert the error tolerance to an amount of decimal places to truncate to 634 | const halfTolerance = tolerance * 0.5; 635 | const exponent = Math.log10( 1 / tolerance ); 636 | const hashMultiplier = Math.pow( 10, exponent ); 637 | const hashAdditive = halfTolerance * hashMultiplier; 638 | for ( let i = 0; i < vertexCount; i ++ ) { 639 | 640 | const index = indices ? indices.getX( i ) : i; 641 | 642 | // Generate a hash for the vertex attributes at the current index 'i' 643 | let hash = ''; 644 | for ( let j = 0, l = attributeNames.length; j < l; j ++ ) { 645 | 646 | const name = attributeNames[ j ]; 647 | const attribute = geometry.getAttribute( name ); 648 | const itemSize = attribute.itemSize; 649 | 650 | for ( let k = 0; k < itemSize; k ++ ) { 651 | 652 | // double tilde truncates the decimal value 653 | hash += `${ ~ ~ ( attribute[ getters[ k ] ]( index ) * hashMultiplier + hashAdditive ) },`; 654 | 655 | } 656 | 657 | } 658 | 659 | // Add another reference to the vertex if it's already 660 | // used by another index 661 | if ( hash in hashToIndex ) { 662 | 663 | newIndices.push( hashToIndex[ hash ] ); 664 | 665 | } else { 666 | 667 | // copy data to the new index in the temporary attributes 668 | for ( let j = 0, l = attributeNames.length; j < l; j ++ ) { 669 | 670 | const name = attributeNames[ j ]; 671 | const attribute = geometry.getAttribute( name ); 672 | const morphAttr = geometry.morphAttributes[ name ]; 673 | const itemSize = attribute.itemSize; 674 | const newarray = tmpAttributes[ name ]; 675 | const newMorphArrays = tmpMorphAttributes[ name ]; 676 | 677 | for ( let k = 0; k < itemSize; k ++ ) { 678 | 679 | const getterFunc = getters[ k ]; 680 | const setterFunc = setters[ k ]; 681 | newarray[ setterFunc ]( nextIndex, attribute[ getterFunc ]( index ) ); 682 | 683 | if ( morphAttr ) { 684 | 685 | for ( let m = 0, ml = morphAttr.length; m < ml; m ++ ) { 686 | 687 | newMorphArrays[ m ][ setterFunc ]( nextIndex, morphAttr[ m ][ getterFunc ]( index ) ); 688 | 689 | } 690 | 691 | } 692 | 693 | } 694 | 695 | } 696 | 697 | hashToIndex[ hash ] = nextIndex; 698 | newIndices.push( nextIndex ); 699 | nextIndex ++; 700 | 701 | } 702 | 703 | } 704 | 705 | // generate result BufferGeometry 706 | const result = geometry.clone(); 707 | for ( const name in geometry.attributes ) { 708 | 709 | const tmpAttribute = tmpAttributes[ name ]; 710 | 711 | result.setAttribute( name, new BufferAttribute( 712 | tmpAttribute.array.slice( 0, nextIndex * tmpAttribute.itemSize ), 713 | tmpAttribute.itemSize, 714 | tmpAttribute.normalized, 715 | ) ); 716 | 717 | if ( ! ( name in tmpMorphAttributes ) ) continue; 718 | 719 | for ( let j = 0; j < tmpMorphAttributes[ name ].length; j ++ ) { 720 | 721 | const tmpMorphAttribute = tmpMorphAttributes[ name ][ j ]; 722 | 723 | result.morphAttributes[ name ][ j ] = new BufferAttribute( 724 | tmpMorphAttribute.array.slice( 0, nextIndex * tmpMorphAttribute.itemSize ), 725 | tmpMorphAttribute.itemSize, 726 | tmpMorphAttribute.normalized, 727 | ); 728 | 729 | } 730 | 731 | } 732 | 733 | // indices 734 | 735 | result.setIndex( newIndices ); 736 | 737 | return result; 738 | 739 | } 740 | 741 | /** 742 | * @param {BufferGeometry} geometry 743 | * @param {number} drawMode 744 | * @return {BufferGeometry} 745 | */ 746 | function toTrianglesDrawMode( geometry, drawMode ) { 747 | 748 | if ( drawMode === TrianglesDrawMode ) { 749 | 750 | console.warn( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Geometry already defined as triangles.' ); 751 | return geometry; 752 | 753 | } 754 | 755 | if ( drawMode === TriangleFanDrawMode || drawMode === TriangleStripDrawMode ) { 756 | 757 | let index = geometry.getIndex(); 758 | 759 | // generate index if not present 760 | 761 | if ( index === null ) { 762 | 763 | const indices = []; 764 | 765 | const position = geometry.getAttribute( 'position' ); 766 | 767 | if ( position !== undefined ) { 768 | 769 | for ( let i = 0; i < position.count; i ++ ) { 770 | 771 | indices.push( i ); 772 | 773 | } 774 | 775 | geometry.setIndex( indices ); 776 | index = geometry.getIndex(); 777 | 778 | } else { 779 | 780 | console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' ); 781 | return geometry; 782 | 783 | } 784 | 785 | } 786 | 787 | // 788 | 789 | const numberOfTriangles = index.count - 2; 790 | const newIndices = []; 791 | 792 | if ( drawMode === TriangleFanDrawMode ) { 793 | 794 | // gl.TRIANGLE_FAN 795 | 796 | for ( let i = 1; i <= numberOfTriangles; i ++ ) { 797 | 798 | newIndices.push( index.getX( 0 ) ); 799 | newIndices.push( index.getX( i ) ); 800 | newIndices.push( index.getX( i + 1 ) ); 801 | 802 | } 803 | 804 | } else { 805 | 806 | // gl.TRIANGLE_STRIP 807 | 808 | for ( let i = 0; i < numberOfTriangles; i ++ ) { 809 | 810 | if ( i % 2 === 0 ) { 811 | 812 | newIndices.push( index.getX( i ) ); 813 | newIndices.push( index.getX( i + 1 ) ); 814 | newIndices.push( index.getX( i + 2 ) ); 815 | 816 | } else { 817 | 818 | newIndices.push( index.getX( i + 2 ) ); 819 | newIndices.push( index.getX( i + 1 ) ); 820 | newIndices.push( index.getX( i ) ); 821 | 822 | } 823 | 824 | } 825 | 826 | } 827 | 828 | if ( ( newIndices.length / 3 ) !== numberOfTriangles ) { 829 | 830 | console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' ); 831 | 832 | } 833 | 834 | // build final geometry 835 | 836 | const newGeometry = geometry.clone(); 837 | newGeometry.setIndex( newIndices ); 838 | newGeometry.clearGroups(); 839 | 840 | return newGeometry; 841 | 842 | } else { 843 | 844 | console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unknown draw mode:', drawMode ); 845 | return geometry; 846 | 847 | } 848 | 849 | } 850 | 851 | /** 852 | * Calculates the morphed attributes of a morphed/skinned BufferGeometry. 853 | * Helpful for Raytracing or Decals. 854 | * @param {Mesh | Line | Points} object An instance of Mesh, Line or Points. 855 | * @return {Object} An Object with original position/normal attributes and morphed ones. 856 | */ 857 | function computeMorphedAttributes( object ) { 858 | 859 | const _vA = new Vector3(); 860 | const _vB = new Vector3(); 861 | const _vC = new Vector3(); 862 | 863 | const _tempA = new Vector3(); 864 | const _tempB = new Vector3(); 865 | const _tempC = new Vector3(); 866 | 867 | const _morphA = new Vector3(); 868 | const _morphB = new Vector3(); 869 | const _morphC = new Vector3(); 870 | 871 | function _calculateMorphedAttributeData( 872 | object, 873 | attribute, 874 | morphAttribute, 875 | morphTargetsRelative, 876 | a, 877 | b, 878 | c, 879 | modifiedAttributeArray 880 | ) { 881 | 882 | _vA.fromBufferAttribute( attribute, a ); 883 | _vB.fromBufferAttribute( attribute, b ); 884 | _vC.fromBufferAttribute( attribute, c ); 885 | 886 | const morphInfluences = object.morphTargetInfluences; 887 | 888 | if ( morphAttribute && morphInfluences ) { 889 | 890 | _morphA.set( 0, 0, 0 ); 891 | _morphB.set( 0, 0, 0 ); 892 | _morphC.set( 0, 0, 0 ); 893 | 894 | for ( let i = 0, il = morphAttribute.length; i < il; i ++ ) { 895 | 896 | const influence = morphInfluences[ i ]; 897 | const morph = morphAttribute[ i ]; 898 | 899 | if ( influence === 0 ) continue; 900 | 901 | _tempA.fromBufferAttribute( morph, a ); 902 | _tempB.fromBufferAttribute( morph, b ); 903 | _tempC.fromBufferAttribute( morph, c ); 904 | 905 | if ( morphTargetsRelative ) { 906 | 907 | _morphA.addScaledVector( _tempA, influence ); 908 | _morphB.addScaledVector( _tempB, influence ); 909 | _morphC.addScaledVector( _tempC, influence ); 910 | 911 | } else { 912 | 913 | _morphA.addScaledVector( _tempA.sub( _vA ), influence ); 914 | _morphB.addScaledVector( _tempB.sub( _vB ), influence ); 915 | _morphC.addScaledVector( _tempC.sub( _vC ), influence ); 916 | 917 | } 918 | 919 | } 920 | 921 | _vA.add( _morphA ); 922 | _vB.add( _morphB ); 923 | _vC.add( _morphC ); 924 | 925 | } 926 | 927 | if ( object.isSkinnedMesh ) { 928 | 929 | object.applyBoneTransform( a, _vA ); 930 | object.applyBoneTransform( b, _vB ); 931 | object.applyBoneTransform( c, _vC ); 932 | 933 | } 934 | 935 | modifiedAttributeArray[ a * 3 + 0 ] = _vA.x; 936 | modifiedAttributeArray[ a * 3 + 1 ] = _vA.y; 937 | modifiedAttributeArray[ a * 3 + 2 ] = _vA.z; 938 | modifiedAttributeArray[ b * 3 + 0 ] = _vB.x; 939 | modifiedAttributeArray[ b * 3 + 1 ] = _vB.y; 940 | modifiedAttributeArray[ b * 3 + 2 ] = _vB.z; 941 | modifiedAttributeArray[ c * 3 + 0 ] = _vC.x; 942 | modifiedAttributeArray[ c * 3 + 1 ] = _vC.y; 943 | modifiedAttributeArray[ c * 3 + 2 ] = _vC.z; 944 | 945 | } 946 | 947 | const geometry = object.geometry; 948 | const material = object.material; 949 | 950 | let a, b, c; 951 | const index = geometry.index; 952 | const positionAttribute = geometry.attributes.position; 953 | const morphPosition = geometry.morphAttributes.position; 954 | const morphTargetsRelative = geometry.morphTargetsRelative; 955 | const normalAttribute = geometry.attributes.normal; 956 | const morphNormal = geometry.morphAttributes.position; 957 | 958 | const groups = geometry.groups; 959 | const drawRange = geometry.drawRange; 960 | let i, j, il, jl; 961 | let group; 962 | let start, end; 963 | 964 | const modifiedPosition = new Float32Array( positionAttribute.count * positionAttribute.itemSize ); 965 | const modifiedNormal = new Float32Array( normalAttribute.count * normalAttribute.itemSize ); 966 | 967 | if ( index !== null ) { 968 | 969 | // indexed buffer geometry 970 | 971 | if ( Array.isArray( material ) ) { 972 | 973 | for ( i = 0, il = groups.length; i < il; i ++ ) { 974 | 975 | group = groups[ i ]; 976 | 977 | start = Math.max( group.start, drawRange.start ); 978 | end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) ); 979 | 980 | for ( j = start, jl = end; j < jl; j += 3 ) { 981 | 982 | a = index.getX( j ); 983 | b = index.getX( j + 1 ); 984 | c = index.getX( j + 2 ); 985 | 986 | _calculateMorphedAttributeData( 987 | object, 988 | positionAttribute, 989 | morphPosition, 990 | morphTargetsRelative, 991 | a, b, c, 992 | modifiedPosition 993 | ); 994 | 995 | _calculateMorphedAttributeData( 996 | object, 997 | normalAttribute, 998 | morphNormal, 999 | morphTargetsRelative, 1000 | a, b, c, 1001 | modifiedNormal 1002 | ); 1003 | 1004 | } 1005 | 1006 | } 1007 | 1008 | } else { 1009 | 1010 | start = Math.max( 0, drawRange.start ); 1011 | end = Math.min( index.count, ( drawRange.start + drawRange.count ) ); 1012 | 1013 | for ( i = start, il = end; i < il; i += 3 ) { 1014 | 1015 | a = index.getX( i ); 1016 | b = index.getX( i + 1 ); 1017 | c = index.getX( i + 2 ); 1018 | 1019 | _calculateMorphedAttributeData( 1020 | object, 1021 | positionAttribute, 1022 | morphPosition, 1023 | morphTargetsRelative, 1024 | a, b, c, 1025 | modifiedPosition 1026 | ); 1027 | 1028 | _calculateMorphedAttributeData( 1029 | object, 1030 | normalAttribute, 1031 | morphNormal, 1032 | morphTargetsRelative, 1033 | a, b, c, 1034 | modifiedNormal 1035 | ); 1036 | 1037 | } 1038 | 1039 | } 1040 | 1041 | } else { 1042 | 1043 | // non-indexed buffer geometry 1044 | 1045 | if ( Array.isArray( material ) ) { 1046 | 1047 | for ( i = 0, il = groups.length; i < il; i ++ ) { 1048 | 1049 | group = groups[ i ]; 1050 | 1051 | start = Math.max( group.start, drawRange.start ); 1052 | end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) ); 1053 | 1054 | for ( j = start, jl = end; j < jl; j += 3 ) { 1055 | 1056 | a = j; 1057 | b = j + 1; 1058 | c = j + 2; 1059 | 1060 | _calculateMorphedAttributeData( 1061 | object, 1062 | positionAttribute, 1063 | morphPosition, 1064 | morphTargetsRelative, 1065 | a, b, c, 1066 | modifiedPosition 1067 | ); 1068 | 1069 | _calculateMorphedAttributeData( 1070 | object, 1071 | normalAttribute, 1072 | morphNormal, 1073 | morphTargetsRelative, 1074 | a, b, c, 1075 | modifiedNormal 1076 | ); 1077 | 1078 | } 1079 | 1080 | } 1081 | 1082 | } else { 1083 | 1084 | start = Math.max( 0, drawRange.start ); 1085 | end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) ); 1086 | 1087 | for ( i = start, il = end; i < il; i += 3 ) { 1088 | 1089 | a = i; 1090 | b = i + 1; 1091 | c = i + 2; 1092 | 1093 | _calculateMorphedAttributeData( 1094 | object, 1095 | positionAttribute, 1096 | morphPosition, 1097 | morphTargetsRelative, 1098 | a, b, c, 1099 | modifiedPosition 1100 | ); 1101 | 1102 | _calculateMorphedAttributeData( 1103 | object, 1104 | normalAttribute, 1105 | morphNormal, 1106 | morphTargetsRelative, 1107 | a, b, c, 1108 | modifiedNormal 1109 | ); 1110 | 1111 | } 1112 | 1113 | } 1114 | 1115 | } 1116 | 1117 | const morphedPositionAttribute = new Float32BufferAttribute( modifiedPosition, 3 ); 1118 | const morphedNormalAttribute = new Float32BufferAttribute( modifiedNormal, 3 ); 1119 | 1120 | return { 1121 | 1122 | positionAttribute: positionAttribute, 1123 | normalAttribute: normalAttribute, 1124 | morphedPositionAttribute: morphedPositionAttribute, 1125 | morphedNormalAttribute: morphedNormalAttribute 1126 | 1127 | }; 1128 | 1129 | } 1130 | 1131 | function mergeGroups( geometry ) { 1132 | 1133 | if ( geometry.groups.length === 0 ) { 1134 | 1135 | console.warn( 'THREE.BufferGeometryUtils.mergeGroups(): No groups are defined. Nothing to merge.' ); 1136 | return geometry; 1137 | 1138 | } 1139 | 1140 | let groups = geometry.groups; 1141 | 1142 | // sort groups by material index 1143 | 1144 | groups = groups.sort( ( a, b ) => { 1145 | 1146 | if ( a.materialIndex !== b.materialIndex ) return a.materialIndex - b.materialIndex; 1147 | 1148 | return a.start - b.start; 1149 | 1150 | } ); 1151 | 1152 | // create index for non-indexed geometries 1153 | 1154 | if ( geometry.getIndex() === null ) { 1155 | 1156 | const positionAttribute = geometry.getAttribute( 'position' ); 1157 | const indices = []; 1158 | 1159 | for ( let i = 0; i < positionAttribute.count; i += 3 ) { 1160 | 1161 | indices.push( i, i + 1, i + 2 ); 1162 | 1163 | } 1164 | 1165 | geometry.setIndex( indices ); 1166 | 1167 | } 1168 | 1169 | // sort index 1170 | 1171 | const index = geometry.getIndex(); 1172 | 1173 | const newIndices = []; 1174 | 1175 | for ( let i = 0; i < groups.length; i ++ ) { 1176 | 1177 | const group = groups[ i ]; 1178 | 1179 | const groupStart = group.start; 1180 | const groupLength = groupStart + group.count; 1181 | 1182 | for ( let j = groupStart; j < groupLength; j ++ ) { 1183 | 1184 | newIndices.push( index.getX( j ) ); 1185 | 1186 | } 1187 | 1188 | } 1189 | 1190 | geometry.dispose(); // Required to force buffer recreation 1191 | geometry.setIndex( newIndices ); 1192 | 1193 | // update groups indices 1194 | 1195 | let start = 0; 1196 | 1197 | for ( let i = 0; i < groups.length; i ++ ) { 1198 | 1199 | const group = groups[ i ]; 1200 | 1201 | group.start = start; 1202 | start += group.count; 1203 | 1204 | } 1205 | 1206 | // merge groups 1207 | 1208 | let currentGroup = groups[ 0 ]; 1209 | 1210 | geometry.groups = [ currentGroup ]; 1211 | 1212 | for ( let i = 1; i < groups.length; i ++ ) { 1213 | 1214 | const group = groups[ i ]; 1215 | 1216 | if ( currentGroup.materialIndex === group.materialIndex ) { 1217 | 1218 | currentGroup.count += group.count; 1219 | 1220 | } else { 1221 | 1222 | currentGroup = group; 1223 | geometry.groups.push( currentGroup ); 1224 | 1225 | } 1226 | 1227 | } 1228 | 1229 | return geometry; 1230 | 1231 | } 1232 | 1233 | 1234 | /** 1235 | * Modifies the supplied geometry if it is non-indexed, otherwise creates a new, 1236 | * non-indexed geometry. Returns the geometry with smooth normals everywhere except 1237 | * faces that meet at an angle greater than the crease angle. 1238 | * 1239 | * @param {BufferGeometry} geometry 1240 | * @param {number} [creaseAngle] 1241 | * @return {BufferGeometry} 1242 | */ 1243 | function toCreasedNormals( geometry, creaseAngle = Math.PI / 3 /* 60 degrees */ ) { 1244 | 1245 | const creaseDot = Math.cos( creaseAngle ); 1246 | const hashMultiplier = ( 1 + 1e-10 ) * 1e2; 1247 | 1248 | // reusable vectors 1249 | const verts = [ new Vector3(), new Vector3(), new Vector3() ]; 1250 | const tempVec1 = new Vector3(); 1251 | const tempVec2 = new Vector3(); 1252 | const tempNorm = new Vector3(); 1253 | const tempNorm2 = new Vector3(); 1254 | 1255 | // hashes a vector 1256 | function hashVertex( v ) { 1257 | 1258 | const x = ~ ~ ( v.x * hashMultiplier ); 1259 | const y = ~ ~ ( v.y * hashMultiplier ); 1260 | const z = ~ ~ ( v.z * hashMultiplier ); 1261 | return `${x},${y},${z}`; 1262 | 1263 | } 1264 | 1265 | // BufferGeometry.toNonIndexed() warns if the geometry is non-indexed 1266 | // and returns the original geometry 1267 | const resultGeometry = geometry.index ? geometry.toNonIndexed() : geometry; 1268 | const posAttr = resultGeometry.attributes.position; 1269 | const vertexMap = {}; 1270 | 1271 | // find all the normals shared by commonly located vertices 1272 | for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) { 1273 | 1274 | const i3 = 3 * i; 1275 | const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 ); 1276 | const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 ); 1277 | const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 ); 1278 | 1279 | tempVec1.subVectors( c, b ); 1280 | tempVec2.subVectors( a, b ); 1281 | 1282 | // add the normal to the map for all vertices 1283 | const normal = new Vector3().crossVectors( tempVec1, tempVec2 ).normalize(); 1284 | for ( let n = 0; n < 3; n ++ ) { 1285 | 1286 | const vert = verts[ n ]; 1287 | const hash = hashVertex( vert ); 1288 | if ( ! ( hash in vertexMap ) ) { 1289 | 1290 | vertexMap[ hash ] = []; 1291 | 1292 | } 1293 | 1294 | vertexMap[ hash ].push( normal ); 1295 | 1296 | } 1297 | 1298 | } 1299 | 1300 | // average normals from all vertices that share a common location if they are within the 1301 | // provided crease threshold 1302 | const normalArray = new Float32Array( posAttr.count * 3 ); 1303 | const normAttr = new BufferAttribute( normalArray, 3, false ); 1304 | for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) { 1305 | 1306 | // get the face normal for this vertex 1307 | const i3 = 3 * i; 1308 | const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 ); 1309 | const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 ); 1310 | const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 ); 1311 | 1312 | tempVec1.subVectors( c, b ); 1313 | tempVec2.subVectors( a, b ); 1314 | 1315 | tempNorm.crossVectors( tempVec1, tempVec2 ).normalize(); 1316 | 1317 | // average all normals that meet the threshold and set the normal value 1318 | for ( let n = 0; n < 3; n ++ ) { 1319 | 1320 | const vert = verts[ n ]; 1321 | const hash = hashVertex( vert ); 1322 | const otherNormals = vertexMap[ hash ]; 1323 | tempNorm2.set( 0, 0, 0 ); 1324 | 1325 | for ( let k = 0, lk = otherNormals.length; k < lk; k ++ ) { 1326 | 1327 | const otherNorm = otherNormals[ k ]; 1328 | if ( tempNorm.dot( otherNorm ) > creaseDot ) { 1329 | 1330 | tempNorm2.add( otherNorm ); 1331 | 1332 | } 1333 | 1334 | } 1335 | 1336 | tempNorm2.normalize(); 1337 | normAttr.setXYZ( i3 + n, tempNorm2.x, tempNorm2.y, tempNorm2.z ); 1338 | 1339 | } 1340 | 1341 | } 1342 | 1343 | resultGeometry.setAttribute( 'normal', normAttr ); 1344 | return resultGeometry; 1345 | 1346 | } 1347 | 1348 | function mergeBufferGeometries( geometries, useGroups = false ) { 1349 | 1350 | console.warn( 'THREE.BufferGeometryUtils: mergeBufferGeometries() has been renamed to mergeGeometries().' ); // @deprecated, r151 1351 | return mergeGeometries( geometries, useGroups ); 1352 | 1353 | } 1354 | 1355 | function mergeBufferAttributes( attributes ) { 1356 | 1357 | console.warn( 'THREE.BufferGeometryUtils: mergeBufferAttributes() has been renamed to mergeAttributes().' ); // @deprecated, r151 1358 | return mergeAttributes( attributes ); 1359 | 1360 | } 1361 | 1362 | export { 1363 | computeMikkTSpaceTangents, 1364 | mergeGeometries, 1365 | mergeBufferGeometries, 1366 | mergeAttributes, 1367 | mergeBufferAttributes, 1368 | interleaveAttributes, 1369 | estimateBytesUsed, 1370 | mergeVertices, 1371 | toTrianglesDrawMode, 1372 | computeMorphedAttributes, 1373 | mergeGroups, 1374 | toCreasedNormals 1375 | }; 1376 | --------------------------------------------------------------------------------