├── .gitignore ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── assets ├── css │ └── main.css └── images │ ├── github.svg │ ├── logo.jpg │ ├── mastodon.svg │ └── twitter.svg ├── index.html ├── package.json ├── src ├── glsl │ ├── constraints.frag.js │ ├── integrate.frag.js │ ├── mouse.frag.js │ ├── normals.frag.js │ ├── through.frag.js │ └── through.vert.js ├── main.js ├── modules │ ├── background.js │ ├── cloth.js │ ├── fbo.js │ ├── lights.js │ ├── materials.js │ ├── mouse.js │ └── pre.js └── textures │ └── bmpMap.png └── utils └── externs.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | /node_modules 4 | .eslintignore 5 | *.js.map 6 | dev.html 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5501 3 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018-2023 ScieCode 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## License 5 | This experiment is under MIT License. 6 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | /* === [IMPORTS] === */ 2 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:600&display=swap'); 3 | 4 | /* === [GLOBAL] === */ 5 | body { 6 | margin: 0; 7 | position: fixed; 8 | } 9 | 10 | canvas { 11 | display: block; 12 | width: 100%; 13 | height: 100%; 14 | z-index: 1; 15 | } 16 | 17 | #footer { 18 | position: absolute; 19 | display: flex; 20 | align-items: center; 21 | right: 4vw; 22 | bottom: 5vh; 23 | user-select: none; 24 | z-index: 10; 25 | } 26 | 27 | .footers { 28 | font-weight: 600; 29 | display: block; 30 | color: #e4e5e6; 31 | font-size: 14px; 32 | text-rendering: optimizeLegibility; 33 | -webkit-font-smoothing: subpixel-antialiased; 34 | font-family: 'Open Sans', sans-serif; 35 | letter-spacing: 1.1px; 36 | user-select: none; 37 | margin: auto 5px; 38 | z-index: 10; 39 | } 40 | 41 | a.footers { 42 | pointer-events: all; 43 | } 44 | 45 | #twitter { 46 | display: block; 47 | height: 16px; 48 | aspect-ratio: 1; 49 | background-color: #e4e5e6; 50 | mask-image: url(../images/twitter.svg); 51 | mask-repeat: no-repeat; 52 | mask-size: contain; 53 | -webkit-mask-image: url(../images/twitter.svg); 54 | -webkit-mask-repeat: no-repeat; 55 | -webkit-mask-size: contain; 56 | } 57 | 58 | #mastodon { 59 | display: block; 60 | height: 16px; 61 | aspect-ratio: 1; 62 | background-color: #e4e5e6; 63 | mask-image: url(../images/mastodon.svg); 64 | mask-repeat: no-repeat; 65 | mask-size: contain; 66 | -webkit-mask-image: url(../images/mastodon.svg); 67 | -webkit-mask-repeat: no-repeat; 68 | -webkit-mask-size: contain; 69 | } 70 | 71 | #github { 72 | display: block; 73 | height: 16px; 74 | aspect-ratio: 1; 75 | background-color: #e4e5e6; 76 | mask-image: url(../images/github.svg); 77 | mask-repeat: no-repeat; 78 | mask-size: contain; 79 | -webkit-mask-image: url(../images/github.svg); 80 | -webkit-mask-repeat: no-repeat; 81 | -webkit-mask-size: contain; 82 | } 83 | -------------------------------------------------------------------------------- /assets/images/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sciecode/sheen/0745512693762050041320a5e4cc6e89daf6b415/assets/images/logo.jpg -------------------------------------------------------------------------------- /assets/images/mastodon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /assets/images/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sheen 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sheen", 3 | "version": "1.0.0", 4 | "description": "GPGPU Cloth Simulation", 5 | "main": "src/main.js", 6 | "author": "sciecode", 7 | "license": "MIT", 8 | "homepage": "sciecode.github.io/sheen", 9 | "devDependencies": { 10 | "http-server": "^0.11.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/glsl/constraints.frag.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */` 2 | precision highp float; 3 | precision highp sampler2D; 4 | 5 | uniform vec2 tSize; 6 | uniform float order; 7 | uniform sampler2D tPosition0; 8 | uniform sampler2D tPosition1; 9 | 10 | uniform sampler2D tDistancesA; 11 | uniform sampler2D tDistancesB; 12 | 13 | uniform sampler2D tAdjacentsA; 14 | uniform sampler2D tAdjacentsB; 15 | 16 | // get vec2 tex coordinate from index 17 | vec2 getUV( float id ) { 18 | 19 | vec2 coords = vec2( 20 | floor( mod( ( id + 0.5 ), tSize.x ) ), 21 | floor( ( id + 0.5 ) / tSize.x ) 22 | ) + 0.5; 23 | 24 | return coords / tSize; 25 | 26 | } 27 | 28 | // compute offset based on current distance and spring rest distance 29 | vec3 getDisplacement( vec3 point0, vec3 point1, float restDistance ) { 30 | 31 | float curDistance = distance( point0, point1 ); 32 | if (curDistance < 0.0001) return vec3(0.0); 33 | return .5 * ( curDistance - restDistance ) * ( point1 - point0 ) / curDistance; 34 | 35 | } 36 | 37 | // pack float16 position into float32 38 | vec3 packPosition( vec2 uv ) { 39 | 40 | return ( texture2D( tPosition0, uv ).xyz + texture2D( tPosition1, uv ).xyz ) / 1024.0; 41 | 42 | } 43 | 44 | vec3 unpackPosition( vec3 pos ) { 45 | 46 | pos *= 1024.0; 47 | 48 | return ( order > 0.0 ) ? floor( pos ) : fract( pos ); 49 | 50 | } 51 | 52 | void main() { 53 | 54 | vec3 displacement; 55 | vec2 uv = gl_FragCoord.xy / tSize.xy; 56 | 57 | // indices of adjacent vertices 58 | vec4 adjacentA = texture2D( tAdjacentsA, uv ); 59 | vec4 adjacentB = texture2D( tAdjacentsB, uv ); 60 | 61 | // distances of adjacent vertices 62 | vec4 distancesA = texture2D( tDistancesA, uv ); 63 | vec4 distancesB = texture2D( tDistancesB, uv ); 64 | 65 | // vertex position 66 | vec3 p0 = packPosition( uv ); 67 | 68 | // adjacent vertices positions 69 | vec3 p1 = packPosition( getUV( adjacentA.x ) ); 70 | vec3 p2 = packPosition( getUV( adjacentA.y ) ); 71 | vec3 p3 = packPosition( getUV( adjacentA.z ) ); 72 | vec3 p4 = packPosition( getUV( adjacentA.w ) ); 73 | vec3 p5 = packPosition( getUV( adjacentB.x ) ); 74 | vec3 p6 = packPosition( getUV( adjacentB.y ) ); 75 | 76 | // spring-based displacement 77 | displacement += getDisplacement( p0, p1, distancesA.x ); 78 | displacement += getDisplacement( p0, p2, distancesA.y ); 79 | displacement += getDisplacement( p0, p3, distancesA.z ); 80 | displacement += getDisplacement( p0, p4, distancesA.w ); 81 | displacement += getDisplacement( p0, p5, distancesB.x ); 82 | displacement += ( adjacentB.y > 0.0 ) ? getDisplacement( p0, p6, distancesB.y ) : vec3( 0 ); 83 | 84 | p0 += displacement / ( ( adjacentB.y > 0.0 ) ? 6.0 : 5.0 ); 85 | 86 | gl_FragColor = vec4( unpackPosition( p0 ), 1.0 ); 87 | 88 | } 89 | `; 90 | -------------------------------------------------------------------------------- /src/glsl/integrate.frag.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */` 2 | precision highp float; 3 | precision highp sampler2D; 4 | 5 | uniform vec2 tSize; 6 | uniform float dt; 7 | uniform float order; 8 | uniform sampler2D tOriginal; 9 | uniform sampler2D tPrevious0; 10 | uniform sampler2D tPrevious1; 11 | uniform sampler2D tPosition0; 12 | uniform sampler2D tPosition1; 13 | 14 | vec3 unpackPosition( vec3 pos ) { 15 | 16 | pos *= 1024.0; 17 | 18 | return ( order > 0.0 ) ? floor( pos ) : fract( pos ); 19 | 20 | } 21 | 22 | void main() { 23 | 24 | float dt2 = dt*dt; 25 | 26 | vec2 uv = gl_FragCoord.xy / tSize.xy; 27 | 28 | vec3 org = texture2D( tOriginal, uv ).xyz; 29 | vec3 prv = ( texture2D( tPrevious0, uv ).xyz + texture2D( tPrevious1, uv ).xyz ) / 1024.0; 30 | vec3 pos = ( texture2D( tPosition0, uv ).xyz + texture2D( tPosition1, uv ).xyz ) / 1024.0; 31 | 32 | vec3 accel = 345.*(org-pos); 33 | vec3 vel = (pos-prv)/dt+accel*dt; 34 | vec3 disp = 0.98*vel*dt; 35 | 36 | gl_FragColor = vec4( unpackPosition( pos + disp ), 1.0 ); 37 | 38 | } 39 | `; 40 | -------------------------------------------------------------------------------- /src/glsl/mouse.frag.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */` 2 | precision highp float; 3 | precision highp sampler2D; 4 | 5 | uniform float vertices[3]; 6 | uniform vec3 coordinates[3]; 7 | 8 | uniform vec2 tSize; 9 | uniform float order; 10 | uniform sampler2D tPosition0; 11 | uniform sampler2D tPosition1; 12 | uniform sampler2D tOriginal; 13 | 14 | // get vec2 tex coordinate from index 15 | vec2 getUV( float id ) { 16 | 17 | vec2 coords = vec2( 18 | floor( mod( ( id + 0.5 ), tSize.x ) ), 19 | floor( ( id + 0.5 ) / tSize.x ) 20 | ) + 0.5; 21 | 22 | return coords / tSize; 23 | 24 | } 25 | 26 | // pack float16 position into float32 27 | vec3 packPosition( vec2 uv ) { 28 | 29 | return ( texture2D( tPosition0, uv ).xyz + texture2D( tPosition1, uv ).xyz ) / 1024.0; 30 | 31 | } 32 | 33 | vec3 unpackPosition( vec3 pos ) { 34 | 35 | pos *= 1024.0; 36 | 37 | return ( order > 0.0 ) ? floor( pos ) : fract( pos ); 38 | 39 | } 40 | 41 | void main() { 42 | 43 | vec2 uv = gl_FragCoord.xy / tSize.xy; 44 | 45 | vec3 pos = packPosition( uv ); 46 | vec3 org = texture2D( tOriginal, uv ).xyz; 47 | 48 | vec3 ref, diff, proj, offset; 49 | 50 | for ( int i = 0; i < 3; ++i ) { 51 | 52 | if ( vertices[ i ] == - 1.0 ) continue; 53 | 54 | ref = texture2D( tOriginal, getUV( vertices[ i ] ) ).xyz; 55 | offset = coordinates[ i ] - ref; 56 | 57 | if ( distance( org, ref ) <= 0.1 ) { 58 | 59 | diff = ref - org; 60 | 61 | proj = dot( diff, offset ) / dot( offset, offset ) * org; 62 | 63 | pos = org + proj + offset; 64 | 65 | } 66 | 67 | } 68 | 69 | gl_FragColor = vec4( unpackPosition( pos ), 1.0 ); 70 | 71 | } 72 | `; 73 | -------------------------------------------------------------------------------- /src/glsl/normals.frag.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */` 2 | precision highp float; 3 | precision highp sampler2D; 4 | 5 | uniform vec2 tSize; 6 | 7 | uniform sampler2D tPosition0; 8 | uniform sampler2D tPosition1; 9 | 10 | uniform sampler2D tAdjacentsA; 11 | uniform sampler2D tAdjacentsB; 12 | 13 | // get vec2 tex coordinate from index 14 | vec2 getUV( float id ) { 15 | 16 | vec2 coords = vec2( 17 | floor( mod( ( id + 0.5 ), tSize.x ) ), 18 | floor( ( id + 0.5 ) / tSize.x ) 19 | ) + 0.5; 20 | 21 | return coords / tSize; 22 | 23 | } 24 | 25 | // pack float16 position into float32 26 | vec3 packPosition( vec2 uv ) { 27 | 28 | return ( texture2D( tPosition0, uv ).xyz + texture2D( tPosition1, uv ).xyz ) / 1024.0; 29 | 30 | } 31 | 32 | void main () { 33 | 34 | vec3 normal; 35 | vec2 uv = gl_FragCoord.xy / tSize.xy; 36 | 37 | // indices of adjacent vertices 38 | vec4 adjacentA = texture2D( tAdjacentsA, uv ); 39 | vec4 adjacentB = texture2D( tAdjacentsB, uv ); 40 | 41 | // vertex position 42 | vec3 p0 = ( texture2D( tPosition0, uv ).xyz + texture2D( tPosition1, uv ).xyz ) / 1024.0; 43 | 44 | // adjacent vertices positions 45 | vec3 p1 = packPosition( getUV( adjacentA.x ) ); 46 | vec3 p2 = packPosition( getUV( adjacentA.y ) ); 47 | vec3 p3 = packPosition( getUV( adjacentA.z ) ); 48 | vec3 p4 = packPosition( getUV( adjacentA.w ) ); 49 | vec3 p5 = packPosition( getUV( adjacentB.x ) ); 50 | vec3 p6 = packPosition( getUV( adjacentB.y ) ); 51 | 52 | // compute vertex normal contribution 53 | normal += cross( p1 - p0, p2 - p0 ); 54 | normal += cross( p2 - p0, p3 - p0 ); 55 | normal += cross( p3 - p0, p4 - p0 ); 56 | normal += cross( p4 - p0, p5 - p0 ); 57 | 58 | if ( adjacentB.y > 0.0 ) { 59 | 60 | normal += cross( p5 - p0, p6 - p0 ); 61 | normal += cross( p6 - p0, p1 - p0 ); 62 | 63 | } else { 64 | 65 | normal += cross( p5 - p0, p1 - p0 ); 66 | 67 | } 68 | 69 | gl_FragColor = vec4( normalize( normal ), 1.0 ); 70 | } 71 | `; 72 | -------------------------------------------------------------------------------- /src/glsl/through.frag.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */` 2 | precision highp float; 3 | precision highp sampler2D; 4 | 5 | uniform vec2 tSize; 6 | uniform float order; 7 | uniform sampler2D texture; 8 | 9 | vec4 unpackPosition( vec4 pos ) { 10 | 11 | pos *= 1024.0; 12 | 13 | return ( order > 0.0 ) ? floor( pos ) : fract( pos ); 14 | 15 | } 16 | 17 | void main() { 18 | 19 | vec2 uv = gl_FragCoord.xy / tSize.xy; 20 | 21 | gl_FragColor = unpackPosition( texture2D( texture, uv ) ); 22 | 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /src/glsl/through.vert.js: -------------------------------------------------------------------------------- 1 | export default /* glsl */` 2 | precision highp float; 3 | 4 | attribute vec2 position; 5 | 6 | void main() { 7 | 8 | gl_Position = vec4( position, vec2(1.0) ); 9 | 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import * as BG from './modules/background.js'; 2 | import * as CLOTH from './modules/cloth.js'; 3 | import * as FBO from './modules/fbo.js'; 4 | import * as PRE from './modules/pre.js'; 5 | import * as LIGHTS from './modules/lights.js'; 6 | import * as MOUSE from './modules/mouse.js'; 7 | 8 | let 9 | renderer, camera, scene, lastOrientation; 10 | 11 | function init() { 12 | 13 | // renderer 14 | renderer = new THREE.WebGLRenderer( { antialias: true } ); 15 | renderer.setSize( window.innerWidth, window.innerHeight ); 16 | 17 | renderer.gammaOutput = true; 18 | renderer.physicallyCorrectLights = true; 19 | 20 | renderer.shadowMap.enabled = true; 21 | renderer.shadowMap.type = THREE.PCFShadowMap; 22 | 23 | document.body.appendChild( renderer.domElement ); 24 | 25 | window.addEventListener( 'resize', onResize ); 26 | 27 | // scene 28 | scene = new THREE.Scene(); 29 | scene.background = new THREE.Color( 0x121312 ); 30 | 31 | // camera 32 | camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 4000 ); 33 | camera.position.set( 0, - 0.5, - 3.5 ); 34 | camera.lookAt( new THREE.Vector3() ); 35 | 36 | // pre-calculate geometry information 37 | PRE.calculate(); 38 | 39 | // initialization block; 40 | BG.init( scene ); 41 | CLOTH.init( scene ); 42 | 43 | MOUSE.init( camera, renderer.domElement ); 44 | FBO.init( renderer ); 45 | 46 | // dispose of calculation data 47 | PRE.dispose(); 48 | 49 | // initialize light 50 | LIGHTS.init( scene ); 51 | 52 | // start program 53 | animate(); 54 | 55 | } 56 | 57 | function animate() { 58 | 59 | if ( window.orientation != lastOrientation ) { 60 | 61 | lastOrientation = window.orientation; 62 | onResize(); 63 | 64 | } 65 | 66 | LIGHTS.update(); 67 | FBO.update(); 68 | 69 | renderer.setRenderTarget( null ); 70 | renderer.render( scene, camera ); 71 | 72 | requestAnimationFrame( animate ); 73 | 74 | } 75 | 76 | function onResize() { 77 | 78 | const w = window.innerWidth; 79 | const h = window.innerHeight; 80 | 81 | camera.aspect = w / h; 82 | camera.updateProjectionMatrix(); 83 | 84 | renderer.setSize( w, h ); 85 | 86 | }; 87 | 88 | init(); 89 | -------------------------------------------------------------------------------- /src/modules/background.js: -------------------------------------------------------------------------------- 1 | function init( scene ) { 2 | 3 | const material = new THREE.MeshPhysicalMaterial( { 4 | 5 | color: 0x393939, 6 | metalness: 0.9, 7 | roughness: 0.4, 8 | dithering: true 9 | 10 | } ); 11 | 12 | const geometry = new THREE.PlaneBufferGeometry( 16000, 16000 ); 13 | 14 | const object = new THREE.Mesh( geometry, material ); 15 | object.receiveShadow = true; 16 | object.rotation.x += Math.PI * 0.9; 17 | object.position.set( 0, - 100, 2000 ); 18 | 19 | scene.add( object ); 20 | 21 | } 22 | 23 | export { init }; 24 | -------------------------------------------------------------------------------- /src/modules/cloth.js: -------------------------------------------------------------------------------- 1 | import * as PRE from './pre.js'; 2 | import * as FBO from './fbo.js'; 3 | 4 | let 5 | RESOLUTION, 6 | mesh; 7 | 8 | function init( scene ) { 9 | 10 | RESOLUTION = Math.ceil( Math.sqrt( PRE.vertices.length ) ); 11 | 12 | const bmp = new THREE.TextureLoader().load( './src/textures/bmpMap.png' ); 13 | bmp.wrapS = THREE.RepeatWrapping; 14 | bmp.wrapT = THREE.RepeatWrapping; 15 | bmp.repeat.set( 2.5, 2.5 ); 16 | 17 | const material = new THREE.MeshPhysicalMaterial( { 18 | 19 | color: 0xffda20, 20 | bumpMap: bmp, 21 | bumpScale: 0.012, 22 | metalness: 0.1, 23 | roughness: 0.6, 24 | clearcoat: 0.8, 25 | clearcoatRoughness: 0.35, 26 | sheen: new THREE.Color( 0.2, 0.2, 1 ).multiplyScalar( 1 / 6 ), 27 | dithering: true 28 | 29 | } ); 30 | 31 | // update cloth material with computed position and normals 32 | material.onBeforeCompile = function ( shader ) { 33 | shader.uniforms.tPosition0 = { value: FBO.positionRT[ 0 ].texture }; 34 | shader.uniforms.tPosition1 = { value: FBO.positionRT[ 1 ].texture }; 35 | shader.uniforms.tNormal = { value: FBO.normalsRT.texture }; 36 | shader.vertexShader = 'precision highp sampler2D;\nuniform sampler2D tPosition0;\nuniform sampler2D tPosition1;\nuniform sampler2D tNormal;\n' + shader.vertexShader; 37 | shader.vertexShader = shader.vertexShader.replace( 38 | '#include ', 39 | `vec3 transformed = ( texture2D( tPosition0, position.xy ).xyz + texture2D( tPosition1, position.xy ).xyz ) / 1024.0; 40 | vec3 objectNormal = normalize( texture2D( tNormal, position.xy ).xyz ); 41 | ` 42 | ); 43 | shader.vertexShader = shader.vertexShader.replace( 44 | '#include ', 45 | '' 46 | ); 47 | }; 48 | 49 | // update depth material for correct shadows 50 | const depthMaterial = new THREE.MeshDepthMaterial(); 51 | depthMaterial.onBeforeCompile = function ( shader ) { 52 | shader.uniforms.tPosition0 = { value: FBO.positionRT[ 0 ].texture }; 53 | shader.uniforms.tPosition1 = { value: FBO.positionRT[ 1 ].texture }; 54 | shader.vertexShader = 'precision highp sampler2D;\nuniform sampler2D tPosition0;\nuniform sampler2D tPosition1;\n' + shader.vertexShader; 55 | shader.vertexShader = shader.vertexShader.replace( 56 | '#include ', 57 | `vec3 transformed = ( texture2D( tPosition0, position.xy ).xyz + texture2D( tPosition1, position.xy ).xyz ) / 1024.0;` 58 | ); 59 | }; 60 | 61 | // fill position with associated texture sampling coordinate 62 | const position = new Float32Array( RESOLUTION * RESOLUTION * 3 ); 63 | for ( let i = 0, il = RESOLUTION * RESOLUTION; i < il; i ++ ) { 64 | 65 | const i3 = i * 3; 66 | position[ i3 + 0 ] = ( i % ( RESOLUTION ) ) / ( RESOLUTION ) + 0.5 / ( RESOLUTION ); 67 | position[ i3 + 1 ] = ~ ~ ( i / ( RESOLUTION ) ) / ( RESOLUTION ) + 0.5 / ( RESOLUTION ); 68 | 69 | } 70 | 71 | const geometry = new THREE.BufferGeometry(); 72 | geometry.setIndex( PRE.geometry.index ); 73 | geometry.addAttribute( 'position', new THREE.BufferAttribute( position, 3 ) ); 74 | geometry.addAttribute( 'uv', PRE.geometry.attributes.uv ); 75 | 76 | mesh = new THREE.Mesh( geometry, material ); 77 | mesh.customDepthMaterial = depthMaterial; 78 | mesh.castShadow = true; 79 | 80 | scene.add( mesh ); 81 | 82 | } 83 | 84 | export { init, mesh }; 85 | -------------------------------------------------------------------------------- /src/modules/fbo.js: -------------------------------------------------------------------------------- 1 | import * as PRE from './pre.js'; 2 | import * as MOUSE from './mouse.js'; 3 | 4 | import { 5 | constraintsShader, 6 | copyShader, 7 | integrateShader, 8 | mouseShader, 9 | normalsShader 10 | } from './materials.js'; 11 | 12 | let 13 | RESOLUTION, 14 | renderer, mesh, targetRT, normalsRT, 15 | originalRT, previousRT, positionRT, 16 | adjacentsRT, distancesRT, 17 | iterations = 5, steps = 6, 18 | prevTime, dt; 19 | 20 | const 21 | tSize = new THREE.Vector2(), 22 | scene = new THREE.Scene(), 23 | camera = new THREE.Camera(); 24 | 25 | function init( WebGLRenderer ) { 26 | 27 | // setup 28 | renderer = WebGLRenderer; 29 | 30 | RESOLUTION = Math.ceil( Math.sqrt( PRE.vertices.length ) ); 31 | tSize.set( RESOLUTION, RESOLUTION ); 32 | 33 | // geometry 34 | const geometry = new THREE.BufferGeometry(); 35 | const positions = new Float32Array( [ 36 | -1.0, -1.0, 37 | 3.0, -1.0, 38 | -1.0, 3.0 39 | ] ); 40 | 41 | geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 2 ) ); 42 | 43 | // mesh 44 | mesh = new THREE.Mesh( geometry, copyShader ); 45 | mesh.frustumCulled = false; 46 | scene.add( mesh ); 47 | 48 | scene.updateMatrixWorld = function() {}; 49 | 50 | adjacentsRT = new Array( 2 ); 51 | distancesRT = new Array( 2 ); 52 | positionRT = new Array( 2 ); 53 | previousRT = new Array( 2 ); 54 | targetRT = new Array( 2 ); 55 | 56 | normalsRT = createRenderTarget(); 57 | 58 | // prepare 59 | createPositionTexture(); 60 | 61 | // setup relaxed vertices conditions 62 | for ( let i = 0; i < 2; i++ ) { 63 | 64 | createAdjacentsTexture( i ); 65 | createDistancesTexture( i ); 66 | 67 | positionRT[ i ] = createRenderTarget(); 68 | previousRT[ i ] = createRenderTarget(); 69 | targetRT[ i ] = createRenderTarget(); 70 | 71 | copyTexture( originalRT, positionRT[ i ], !i ); 72 | copyTexture( originalRT, previousRT[ i ], !i ); 73 | 74 | } 75 | 76 | prevTime = performance.now(); 77 | 78 | } 79 | 80 | function copyTexture( input, output, order ) { 81 | 82 | mesh.material = copyShader; 83 | copyShader.uniforms.order.value = ( order ) ? 1 : - 1; 84 | copyShader.uniforms.tSize.value = tSize; 85 | copyShader.uniforms.texture.value = input.texture; 86 | 87 | renderer.setRenderTarget( output ); 88 | renderer.render( scene, camera ); 89 | 90 | } 91 | 92 | function createRenderTarget( ) { 93 | 94 | return new THREE.WebGLRenderTarget( RESOLUTION, RESOLUTION, { 95 | wrapS: THREE.ClampToEdgeWrapping, 96 | wrapT: THREE.ClampToEdgeWrapping, 97 | minFilter: THREE.NearestFilter, 98 | magFilter: THREE.NearestFilter, 99 | format: THREE.RGBAFormat, 100 | type: THREE.HalfFloatType, 101 | depthTest: false, 102 | depthWrite: false, 103 | depthBuffer: false, 104 | stencilBuffer: false 105 | } ); 106 | 107 | } 108 | 109 | function createPositionTexture( ) { 110 | 111 | const data = new Float32Array( RESOLUTION * RESOLUTION * 4 ); 112 | const length = PRE.vertices.length; 113 | 114 | for ( let i = 0; i < length; i++ ) { 115 | 116 | const i4 = i * 4; 117 | 118 | data[ i4 + 0 ] = PRE.vertices[ i ].x; 119 | data[ i4 + 1 ] = PRE.vertices[ i ].y; 120 | data[ i4 + 2 ] = PRE.vertices[ i ].z; 121 | 122 | } 123 | 124 | const tmp = {}; 125 | tmp.texture = new THREE.DataTexture( data, RESOLUTION, RESOLUTION, THREE.RGBAFormat, THREE.FloatType ); 126 | tmp.texture.needsUpdate = true; 127 | 128 | originalRT = tmp; 129 | 130 | } 131 | 132 | function createAdjacentsTexture( k ) { 133 | 134 | const data = new Float32Array( RESOLUTION * RESOLUTION * 4 ); 135 | const length = PRE.vertices.length; 136 | 137 | for ( let i = 0; i < length; i++ ) { 138 | 139 | const i4 = i * 4; 140 | const adj = PRE.adjacency[ i ]; 141 | const len = PRE.adjacency[ i ].length - 1; 142 | 143 | for ( let j = 0; j < 4; j++ ) 144 | data[ i4 + j ] = ( len < k * 4 + j ) ? - 1 : adj[ k * 4 + j ]; 145 | 146 | } 147 | 148 | const tmp = {}; 149 | tmp.texture = new THREE.DataTexture( data, RESOLUTION, RESOLUTION, THREE.RGBAFormat, THREE.FloatType ); 150 | tmp.texture.needsUpdate = true; 151 | 152 | adjacentsRT[ k ] = tmp; 153 | 154 | } 155 | 156 | function createDistancesTexture( k ) { 157 | 158 | const data = new Float32Array( RESOLUTION * RESOLUTION * 4 ); 159 | const length = PRE.vertices.length; 160 | 161 | const vert = PRE.vertices; 162 | 163 | for ( let i = 0; i < length; i++ ) { 164 | 165 | const i4 = i * 4; 166 | const adj = PRE.adjacency[ i ]; 167 | const len = PRE.adjacency[ i ].length - 1; 168 | 169 | const v = vert[ i ]; 170 | 171 | for ( let j = 0; j < 4; j++ ) 172 | data[ i4 + j ] = ( len < k * 4 + j ) ? - 1 : v.distanceTo( vert[ adj[ k * 4 + j ] ] ); 173 | 174 | } 175 | 176 | const tmp = {}; 177 | tmp.texture = new THREE.DataTexture( data, RESOLUTION, RESOLUTION, THREE.RGBAFormat, THREE.FloatType ); 178 | tmp.texture.needsUpdate = true; 179 | 180 | distancesRT[ k ] = tmp; 181 | 182 | } 183 | 184 | function integrate() { 185 | 186 | mesh.material = integrateShader; 187 | integrateShader.uniforms.tSize.value = tSize; 188 | integrateShader.uniforms.dt.value = dt; 189 | integrateShader.uniforms.tOriginal.value = originalRT.texture; 190 | integrateShader.uniforms.tPrevious0.value = previousRT[ 0 ].texture; 191 | integrateShader.uniforms.tPrevious1.value = previousRT[ 1 ].texture; 192 | integrateShader.uniforms.tPosition0.value = positionRT[ 0 ].texture; 193 | integrateShader.uniforms.tPosition1.value = positionRT[ 1 ].texture; 194 | 195 | // console.log(dt); 196 | 197 | // integer-part 198 | integrateShader.uniforms.order.value = 1; 199 | renderer.setRenderTarget( targetRT[ 0 ] ); 200 | renderer.render( scene, camera ); 201 | 202 | // fraction-part 203 | integrateShader.uniforms.order.value = -1; 204 | renderer.setRenderTarget( targetRT[ 1 ] ); 205 | renderer.render( scene, camera ); 206 | 207 | 208 | // swap framebuffers 209 | let tmp = previousRT[ 0 ]; 210 | previousRT[ 0 ] = positionRT[ 0 ]; 211 | positionRT[ 0 ] = targetRT[ 0 ]; 212 | targetRT[ 0 ] = tmp; 213 | 214 | tmp = previousRT[ 1 ]; 215 | previousRT[ 1 ] = positionRT[ 1 ]; 216 | positionRT[ 1 ] = targetRT[ 1 ]; 217 | targetRT[ 1 ] = tmp; 218 | 219 | } 220 | 221 | function solveConstraints() { 222 | 223 | mesh.material = constraintsShader; 224 | constraintsShader.uniforms.tSize.value = tSize; 225 | constraintsShader.uniforms.tPosition0.value = positionRT[ 0 ].texture; 226 | constraintsShader.uniforms.tPosition1.value = positionRT[ 1 ].texture; 227 | constraintsShader.uniforms.tAdjacentsA.value = adjacentsRT[ 0 ].texture; 228 | constraintsShader.uniforms.tAdjacentsB.value = adjacentsRT[ 1 ].texture; 229 | constraintsShader.uniforms.tDistancesA.value = distancesRT[ 0 ].texture; 230 | constraintsShader.uniforms.tDistancesB.value = distancesRT[ 1 ].texture; 231 | 232 | // integer-part 233 | constraintsShader.uniforms.order.value = 1; 234 | renderer.setRenderTarget( targetRT[ 0 ] ); 235 | renderer.render( scene, camera ); 236 | 237 | // fraction-part 238 | constraintsShader.uniforms.order.value = -1; 239 | renderer.setRenderTarget( targetRT[ 1 ] ); 240 | renderer.render( scene, camera ); 241 | 242 | 243 | // swap framebuffers 244 | let tmp = positionRT[ 0 ]; 245 | positionRT[ 0 ] = targetRT [ 0 ]; 246 | targetRT[ 0 ] = tmp; 247 | 248 | tmp = positionRT[ 1 ]; 249 | positionRT[ 1 ] = targetRT [ 1 ]; 250 | targetRT[ 1 ] = tmp; 251 | 252 | } 253 | 254 | function mouseOffset() { 255 | 256 | mesh.material = mouseShader; 257 | mouseShader.uniforms.tSize.value = tSize; 258 | mouseShader.uniforms.vertices.value = MOUSE.vertices; 259 | mouseShader.uniforms.coordinates.value = MOUSE.coordinates; 260 | mouseShader.uniforms.tOriginal.value = originalRT.texture; 261 | mouseShader.uniforms.tPosition0.value = positionRT[ 0 ].texture; 262 | mouseShader.uniforms.tPosition1.value = positionRT[ 1 ].texture; 263 | 264 | // integer-part 265 | mouseShader.uniforms.order.value = 1; 266 | renderer.setRenderTarget( targetRT[ 0 ] ); 267 | renderer.render( scene, camera ); 268 | 269 | // fraction-part 270 | mouseShader.uniforms.order.value = -1; 271 | renderer.setRenderTarget( targetRT[ 1 ] ); 272 | renderer.render( scene, camera ); 273 | 274 | 275 | // swap framebuffers 276 | let tmp = positionRT[ 0 ]; 277 | positionRT[ 0 ] = targetRT [ 0 ]; 278 | targetRT[ 0 ] = tmp; 279 | 280 | tmp = positionRT[ 1 ]; 281 | positionRT[ 1 ] = targetRT [ 1 ]; 282 | targetRT[ 1 ] = tmp; 283 | 284 | } 285 | 286 | function computeVertexNormals( ) { 287 | 288 | mesh.material = normalsShader; 289 | normalsShader.uniforms.tSize.value = tSize; 290 | normalsShader.uniforms.tPosition0.value = positionRT[ 0 ].texture; 291 | normalsShader.uniforms.tPosition1.value = positionRT[ 1 ].texture; 292 | normalsShader.uniforms.tAdjacentsA.value = adjacentsRT[ 0 ].texture; 293 | normalsShader.uniforms.tAdjacentsB.value = adjacentsRT[ 1 ].texture; 294 | 295 | renderer.setRenderTarget( normalsRT ); 296 | renderer.render( scene, camera ); 297 | 298 | } 299 | 300 | function update() { 301 | 302 | const now = performance.now(); 303 | const frame_dt = Math.min(now - prevTime, 33.333)/1000; 304 | dt = frame_dt/steps; 305 | prevTime = now; 306 | 307 | let mouseUpdating = MOUSE.updating(); 308 | 309 | for ( let i = 0; i < steps; i++ ) { 310 | integrate(); 311 | for ( let j = 0; j < iterations; j++ ) { 312 | if ( mouseUpdating && (i 1 && time < 4 ) { 65 | 66 | updateLights( time ); 67 | 68 | } else if ( time > 4 ) { 69 | 70 | updateLights( 4 ); 71 | 72 | finished = true; 73 | 74 | } 75 | 76 | } 77 | 78 | export { init, update }; 79 | -------------------------------------------------------------------------------- /src/modules/materials.js: -------------------------------------------------------------------------------- 1 | // shader-import-block 2 | import through_vert from '../glsl/through.vert.js'; 3 | 4 | import constraints_frag from '../glsl/constraints.frag.js'; 5 | import integrate_frag from '../glsl/integrate.frag.js'; 6 | import mouse_frag from '../glsl/mouse.frag.js'; 7 | import normals_frag from '../glsl/normals.frag.js'; 8 | import through_frag from '../glsl/through.frag.js'; 9 | 10 | 11 | // copyToRenderTarget 12 | const copyShader = new THREE.RawShaderMaterial( { 13 | uniforms: { 14 | order: {}, 15 | tSize: { type: 'v2' }, 16 | texture: { type: 't' } 17 | }, 18 | vertexShader: through_vert, 19 | fragmentShader: through_frag, 20 | fog: false, 21 | lights: false, 22 | depthWrite: false, 23 | depthTest: false 24 | }); 25 | 26 | // forward-integration 27 | const integrateShader = copyShader.clone(); 28 | integrateShader.fragmentShader = integrate_frag; 29 | integrateShader.uniforms = { 30 | dt: { type: 'f' }, 31 | tSize: { type: 'v2' }, 32 | order: {}, 33 | tOriginal: { type: 't' }, 34 | tPrevious0: { type: 't' }, 35 | tPrevious1: { type: 't' }, 36 | tPosition0: { type: 't' }, 37 | tPosition1: { type: 't' } 38 | }; 39 | 40 | // mouse displacement 41 | const mouseShader = copyShader.clone(); 42 | mouseShader.fragmentShader = mouse_frag; 43 | mouseShader.uniforms = { 44 | vertices: { value: null }, 45 | coordinates: { type: 'v3' }, 46 | order: {}, 47 | tSize: { type: 'v2' }, 48 | tOriginal: { type: 't' }, 49 | tPosition0: { type: 't' }, 50 | tPosition1: { type: 't' } 51 | }; 52 | 53 | // vertices relaxation 54 | const constraintsShader = copyShader.clone(); 55 | constraintsShader.fragmentShader = constraints_frag; 56 | constraintsShader.uniforms = { 57 | tSize: { type: 'v2' }, 58 | order: { }, 59 | tPosition0: { type: 't' }, 60 | tPosition1: { type: 't' }, 61 | tAdjacentsA: { type: 't' }, 62 | tAdjacentsB: { type: 't' }, 63 | tDistancesA: { type: 't' }, 64 | tDistancesB: { type: 't' } 65 | }; 66 | 67 | // calculate normals 68 | const normalsShader = copyShader.clone(); 69 | normalsShader.fragmentShader = normals_frag; 70 | normalsShader.uniforms = { 71 | tSize: { type: 'v2' }, 72 | tPosition0: { type: 't' }, 73 | tPosition1: { type: 't' }, 74 | tAdjacentsA: { type: 't' }, 75 | tAdjacentsB: { type: 't' } 76 | 77 | }; 78 | 79 | export { copyShader, integrateShader, mouseShader, constraintsShader, normalsShader } 80 | -------------------------------------------------------------------------------- /src/modules/mouse.js: -------------------------------------------------------------------------------- 1 | import * as PRE from './pre.js'; 2 | 3 | let camera; 4 | 5 | const 6 | pointers = {}, 7 | vertices = new Array( 3 ), 8 | coordinates = new Array( 3 ), 9 | tmpmouse = new THREE.Vector3(), 10 | mouse3d = new THREE.Vector3(), 11 | raycaster = new THREE.Raycaster(), 12 | plane = new THREE.Plane( undefined, -1.8 ), 13 | sphere = new THREE.Sphere( undefined, 1 ); 14 | 15 | 16 | function init( PerspectiveCamera ) { 17 | 18 | camera = PerspectiveCamera; 19 | plane.normal.copy( camera.position ).normalize(); 20 | 21 | const canvas = document.getElementsByTagName( 'canvas' )[0]; 22 | 23 | canvas.addEventListener('mousemove', onMouseMove ); 24 | canvas.addEventListener('mousedown', onMouseDown ); 25 | canvas.addEventListener('mouseout', onMouseOut ); 26 | canvas.addEventListener('mouseup', onMouseUp ); 27 | 28 | canvas.addEventListener('touchmove', onTouchMove, { passive: false } ); 29 | canvas.addEventListener('touchstart', onTouchDown, { passive: false } ); 30 | canvas.addEventListener('touchend', onTouchUp ); 31 | 32 | } 33 | 34 | function updating() { 35 | 36 | let count = 0; 37 | let isUpdating = false; 38 | 39 | for ( let [ key, value ] of Object.entries( pointers ) ) { 40 | 41 | let mouse = value.screenCoordinate; 42 | 43 | raycaster.setFromCamera( mouse, camera ); 44 | 45 | if ( value.vertex === undefined && 46 | raycaster.ray.intersectSphere( sphere, tmpmouse ) != null ) { 47 | 48 | mouse3d.copy( tmpmouse ); 49 | 50 | let dist = Infinity; 51 | 52 | for ( let i = 0; i < PRE.vertices.length; ++i ) { 53 | 54 | const tmp = mouse3d.distanceTo( PRE.vertices[ i ] ); 55 | 56 | if ( tmp < dist ) { 57 | 58 | dist = tmp; 59 | value.vertex = i; 60 | 61 | } 62 | 63 | } 64 | 65 | } 66 | 67 | if ( value.vertex !== undefined ) { 68 | 69 | isUpdating = true; 70 | raycaster.ray.intersectPlane( plane, tmpmouse ); 71 | value.worldCoordinate.copy( tmpmouse ); 72 | 73 | vertices[ count ] = value.vertex; 74 | coordinates[ count ] = value.worldCoordinate; 75 | 76 | count++; 77 | 78 | } 79 | 80 | } 81 | 82 | while ( count < 3 ) { 83 | 84 | vertices[ count ] = -1; 85 | coordinates[ count ] = mouse3d; 86 | 87 | count++; 88 | 89 | } 90 | 91 | return ( isUpdating ) ? true : false; 92 | 93 | } 94 | 95 | function onMouseMove( evt ) { 96 | 97 | if ( pointers[ 'mouse' ] !== undefined ) { 98 | 99 | pointers[ 'mouse' ].screenCoordinate.x = ( evt.pageX / window.innerWidth ) * 2 - 1; 100 | pointers[ 'mouse' ].screenCoordinate.y = - ( evt.pageY / window.innerHeight ) * 2 + 1; 101 | 102 | } 103 | 104 | } 105 | 106 | function onMouseDown( evt ) { 107 | 108 | if ( evt.button == 0 ) { 109 | 110 | pointers[ 'mouse' ] = { 111 | 112 | vertex: undefined, 113 | screenCoordinate: new THREE.Vector2(), 114 | worldCoordinate: new THREE.Vector3() 115 | 116 | } 117 | 118 | onMouseMove( evt ); 119 | 120 | } 121 | 122 | } 123 | 124 | function onMouseUp( evt ) { 125 | 126 | if ( evt.button == 0 ) { 127 | 128 | delete pointers[ 'mouse' ]; 129 | 130 | } 131 | 132 | } 133 | 134 | function onMouseOut() { 135 | 136 | delete pointers[ 'mouse' ]; 137 | 138 | } 139 | 140 | function onTouchMove( evt ) { 141 | 142 | evt.preventDefault(); 143 | 144 | for ( let i = 0; i < evt.changedTouches.length; ++i ) { 145 | 146 | let touch = evt.changedTouches[ i ]; 147 | 148 | pointers[ touch.identifier ].screenCoordinate.x = ( touch.pageX / window.innerWidth ) * 2 - 1; 149 | pointers[ touch.identifier ].screenCoordinate.y = - ( touch.pageY / window.innerHeight ) * 2 + 1; 150 | 151 | } 152 | 153 | } 154 | 155 | function onTouchDown( evt ) { 156 | 157 | for ( let i = 0; i < evt.changedTouches.length; ++i ) { 158 | 159 | let touch = evt.changedTouches[ i ]; 160 | 161 | pointers[ touch.identifier ] = { 162 | 163 | vertex: undefined, 164 | screenCoordinate: new THREE.Vector2(), 165 | worldCoordinate: new THREE.Vector3() 166 | 167 | } 168 | 169 | } 170 | 171 | onTouchMove( evt ); 172 | 173 | } 174 | 175 | function onTouchUp( evt ) { 176 | 177 | for ( let i = 0; i < evt.changedTouches.length; ++i ) { 178 | 179 | let touch = evt.changedTouches[ i ]; 180 | 181 | delete pointers[ touch.identifier ]; 182 | 183 | } 184 | 185 | } 186 | 187 | export { init, updating, vertices, coordinates } 188 | -------------------------------------------------------------------------------- /src/modules/pre.js: -------------------------------------------------------------------------------- 1 | 2 | let 3 | geometry, adjacency, vertices; 4 | 5 | function mergeVertices( geometry, tolerance = 1e-4 ) { 6 | 7 | tolerance = Math.max( tolerance, Number.EPSILON ); 8 | 9 | // Generate an index buffer if the geometry doesn't have one, or optimize it 10 | // if it's already available. 11 | const hashToIndex = {}; 12 | const indices = geometry.getIndex(); 13 | const positions = geometry.getAttribute( 'position' ); 14 | const vertexCount = indices ? indices.count : positions.count; 15 | 16 | // next value for triangle indices 17 | let nextIndex = 0; 18 | 19 | // attributes and new attribute arrays 20 | const attributeNames = Object.keys( geometry.attributes ); 21 | const tmpAttributes = {}; 22 | const tmpMorphAttributes = {}; 23 | const newIndices = []; 24 | const getters = [ 'getX', 'getY', 'getZ', 'getW' ]; 25 | const setters = [ 'setX', 'setY', 'setZ', 'setW' ]; 26 | 27 | // Initialize the arrays, allocating space conservatively. Extra 28 | // space will be trimmed in the last step. 29 | for ( let i = 0, l = attributeNames.length; i < l; i ++ ) { 30 | 31 | const name = attributeNames[ i ]; 32 | const attr = geometry.attributes[ name ]; 33 | tmpAttributes[ name ] = new THREE.BufferAttribute( new attr.array.constructor( attr.count * attr.itemSize ), attr.itemSize, attr.normalized ); 34 | const morphAttr = geometry.morphAttributes[ name ]; 35 | if ( morphAttr ) { 36 | 37 | tmpMorphAttributes[ name ] = new THREE.BufferAttribute( new morphAttr.array.constructor( morphAttr.count * morphAttr.itemSize ), morphAttr.itemSize, morphAttr.normalized ); 38 | 39 | } 40 | 41 | } 42 | 43 | // convert the error tolerance to an amount of decimal places to truncate to 44 | const decimalShift = Math.log10( 1 / tolerance ); 45 | const shiftMultiplier = Math.pow( 10, decimalShift ); 46 | for ( let i = 0; i < vertexCount; i ++ ) { 47 | 48 | const index = indices ? indices.getX( i ) : i; 49 | 50 | // Generate a hash for the vertex attributes at the current index 'i' 51 | let hash = ''; 52 | for ( let j = 0, l = attributeNames.length; j < l; j ++ ) { 53 | 54 | const name = attributeNames[ j ]; 55 | const attribute = geometry.getAttribute( name ); 56 | const itemSize = attribute.itemSize; 57 | for ( let k = 0; k < itemSize; k ++ ) { 58 | 59 | // double tilde truncates the decimal value 60 | hash += `${~ ~ ( attribute[ getters[ k ] ]( index ) * shiftMultiplier )},`; 61 | 62 | } 63 | 64 | } 65 | 66 | // Add another reference to the vertex if it's already 67 | // used by another index 68 | if ( hash in hashToIndex ) { 69 | 70 | newIndices.push( hashToIndex[ hash ] ); 71 | 72 | } else { 73 | 74 | // copy data to the new index in the temporary attributes 75 | for ( let j = 0, l = attributeNames.length; j < l; j ++ ) { 76 | 77 | const name = attributeNames[ j ]; 78 | const attribute = geometry.getAttribute( name ); 79 | const morphAttr = geometry.morphAttributes[ name ]; 80 | const itemSize = attribute.itemSize; 81 | const newarray = tmpAttributes[ name ]; 82 | const newMorphArrays = tmpMorphAttributes[ name ]; 83 | for ( let k = 0; k < itemSize; k ++ ) { 84 | 85 | const getterFunc = getters[ k ]; 86 | const setterFunc = setters[ k ]; 87 | newarray[ setterFunc ]( nextIndex, attribute[ getterFunc ]( index ) ); 88 | if ( morphAttr ) { 89 | 90 | for ( let m = 0, ml = morphAttr.length; m < ml; m ++ ) { 91 | 92 | newMorphArrays[ m ][ setterFunc ]( nextIndex, morphAttr[ m ][ getterFunc ]( index ) ); 93 | 94 | } 95 | 96 | } 97 | 98 | } 99 | 100 | } 101 | 102 | hashToIndex[ hash ] = nextIndex; 103 | newIndices.push( nextIndex ); 104 | nextIndex ++; 105 | 106 | } 107 | 108 | } 109 | 110 | // generate result THREE.BufferGeometry 111 | const result = geometry.clone(); 112 | for ( const name in geometry.attributes ) { 113 | 114 | const tmpAttribute = tmpAttributes[ name ]; 115 | result.setAttribute( name, new THREE.BufferAttribute( tmpAttribute.array.slice( 0, nextIndex * tmpAttribute.itemSize ), tmpAttribute.itemSize, tmpAttribute.normalized ) ); 116 | if ( ! ( name in tmpMorphAttributes ) ) continue; 117 | for ( let j = 0; j < tmpMorphAttributes[ name ].length; j ++ ) { 118 | 119 | const tmpMorphAttribute = tmpMorphAttributes[ name ][ j ]; 120 | result.morphAttributes[ name ][ j ] = new THREE.BufferAttribute( tmpMorphAttribute.array.slice( 0, nextIndex * tmpMorphAttribute.itemSize ), tmpMorphAttribute.itemSize, tmpMorphAttribute.normalized ); 121 | 122 | } 123 | 124 | } 125 | 126 | // indices 127 | 128 | result.setIndex( newIndices ); 129 | return result; 130 | 131 | } 132 | 133 | function calculate() { 134 | 135 | const tmp = new THREE.IcosahedronBufferGeometry( 1000, 5 ); 136 | 137 | // icosahedron generates non-indexed vertices, we make use of graph adjacency. 138 | geometry = mergeVertices( tmp, 1.2 ); 139 | 140 | geometry.scale( 0.00095, 0.00095, 0.00095 ); 141 | 142 | populateVertices(); 143 | 144 | populateAdjacency(); 145 | 146 | } 147 | 148 | function populateVertices() { 149 | 150 | const v0 = new THREE.Vector3(); 151 | const position = geometry.attributes.position; 152 | 153 | vertices = new Array(); 154 | 155 | for ( let i = 0, il = position.count; i < il; i++ ) { 156 | 157 | v0.fromBufferAttribute( position, i ); 158 | vertices.push( v0.clone() ); 159 | 160 | } 161 | 162 | } 163 | 164 | function populateAdjacency() { 165 | 166 | const 167 | index = geometry.index, 168 | faces = Array.from( { length: vertices.length }, () => new Array() ); 169 | 170 | // compute all faces for set vertex 171 | for ( let i = 0, il = index.count / 3; i < il; i++ ) { 172 | 173 | const 174 | i3 = i * 3, 175 | a = index.getX( i3 + 0 ), 176 | b = index.getX( i3 + 1 ), 177 | c = index.getX( i3 + 2 ), 178 | 179 | face = new THREE.Face3( a, b, c ); 180 | 181 | faces[ a ].push( face ); 182 | faces[ b ].push( face ); 183 | faces[ c ].push( face ); 184 | 185 | } 186 | 187 | // support function - find face with winding order ( first ) -> ( next ) 188 | function getFace( arr, first, next ) { 189 | 190 | for ( let r = 0; r < arr.length; r++ ) { 191 | 192 | var n = arr[ r ]; 193 | 194 | if ( n.a === first && n.b === next || 195 | n.b === first && n.c === next || 196 | n.c === first && n.a === next ) 197 | return n 198 | } 199 | 200 | console.error( "sheen.error: shouldn't reach here." ); 201 | return 202 | 203 | } 204 | 205 | adjacency = Array.from( { length: vertices.length }, () => new Array() ); 206 | 207 | // compute sorted adjacency list for every vertex 208 | for ( let r = 0; r < faces.length; r++ ) { 209 | 210 | let n = faces[ r ][ 0 ]; 211 | 212 | // cycle in a fan, through all faces of the vertex 213 | while ( true ) { 214 | 215 | if ( n.a == r ) { 216 | 217 | adjacency[ r ].push( n.c ); 218 | n = getFace( faces[ r ], r, n.c ); // face with reverse winding order ( a ) -> ( c ) 219 | 220 | } else if ( n.b == r ) { 221 | 222 | adjacency[ r ].push( n.a ); 223 | n = getFace( faces[ r ], r, n.a ); // face with reverse winding order ( b ) -> ( a ) 224 | 225 | } else { // n.c == r 226 | 227 | adjacency[ r ].push( n.b ); 228 | n = getFace( faces[ r ], r, n.b ); // face with reverse winding order ( c ) -> ( b ) 229 | 230 | } 231 | 232 | // back to the start - end 233 | if ( n == faces[ r ][ 0 ] ) break; 234 | 235 | } 236 | 237 | } 238 | 239 | } 240 | 241 | function dispose() { 242 | 243 | geometry = undefined; 244 | adjacency = undefined; 245 | 246 | } 247 | 248 | export { calculate, dispose, geometry, vertices, adjacency }; 249 | -------------------------------------------------------------------------------- /src/textures/bmpMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sciecode/sheen/0745512693762050041320a5e4cc6e89daf6b415/src/textures/bmpMap.png -------------------------------------------------------------------------------- /utils/externs.js: -------------------------------------------------------------------------------- 1 | var THREE; 2 | --------------------------------------------------------------------------------