├── 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 | [](https://twitter.com/SketchpunkLabs)
4 | [](https://mastodon.gamedev.place/@sketchpunk)
5 | [](https://bsky.app/profile/sketchpunk.bsky.social)
6 | [](https://www.threads.net/@sketchpunklabs)
7 |
8 |
9 | [](https://youtube.com/c/sketchpunklabs)
10 | [](https://github.com/sponsors/sketchpunklabs)
11 | [](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 |
--------------------------------------------------------------------------------