├── .babelrc ├── .gitignore ├── .parcelrc ├── README.md ├── package.json ├── screenshot.png ├── src ├── GlobalTypes.ts ├── HelloWorldPass.ts ├── PixelatePass.ts ├── RenderPixelatedPass.ts ├── assets │ ├── TileCrate.png │ ├── mech.fbx │ ├── mech2.fbx │ ├── smoothTest1.fbx │ ├── smoothTest2.fbx │ ├── smoothTest3.fbx │ └── warningStripes.png ├── index.html ├── index.ts └── math.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript" 5 | ] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.parcel-cache 2 | /dist 3 | /node_modules -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "@parcel/config-default", 4 | "transformers": { 5 | "*.png": ["@parcel/transformer-raw"], 6 | "*.svg": ["@parcel/transformer-raw"], 7 | "*.fbx": ["@parcel/transformer-raw"], 8 | } 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hello-threejs 2 | A 3D pixel-art style shader with clean outlines and edge highlights. 3 | 4 | This was built for a quick demo, so it's not in a super usable state, but feel free to adapt and use this however you like! 5 | 6 | [Here's a video.](https://www.youtube.com/watch?v=jFevm02NJ5M) 7 | 8 |  9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-threejs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "three": "^0.131.3" 8 | }, 9 | "scripts": { 10 | "watch": "parcel src/index.html" 11 | }, 12 | "devDependencies": { 13 | "@types/three": "^0.130.1", 14 | "parcel": "^2.0.0-rc.0", 15 | "@babel/core": "^7.14.8", 16 | "@babel/preset-typescript": "^7.14.5", 17 | "@babel/preset-env": "^7.14.9" 18 | } 19 | } -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KodyJKing/hello-threejs/c47979c4025f494d403574bbfb08da87de58f94c/screenshot.png -------------------------------------------------------------------------------- /src/GlobalTypes.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { const content: string; export default content } 2 | declare module '*.png' { const content: string; export default content } 3 | declare module '*.fbx' { const content: string; export default content } -------------------------------------------------------------------------------- /src/HelloWorldPass.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three" 2 | import { WebGLRenderer, WebGLRenderTarget } from "three" 3 | import { Pass, FullScreenQuad } from "three/examples/jsm/postprocessing/Pass" 4 | 5 | // Just inverts the output. 6 | export default class HelloWorldPass extends Pass { 7 | 8 | fsQuad: FullScreenQuad 9 | 10 | constructor() { 11 | super() 12 | this.fsQuad = new FullScreenQuad( this.material() ) 13 | } 14 | 15 | render( 16 | renderer: WebGLRenderer, 17 | writeBuffer: WebGLRenderTarget, 18 | readBuffer: WebGLRenderTarget 19 | ) { 20 | // The declarations for Three.js don't include Material.uniforms 21 | // @ts-ignore 22 | this.fsQuad.material.uniforms.tDiffuse.value = readBuffer.texture 23 | if ( this.renderToScreen ) { 24 | renderer.setRenderTarget( null ) 25 | } else { 26 | renderer.setRenderTarget( writeBuffer ) 27 | if ( this.clear ) renderer.clear() 28 | } 29 | this.fsQuad.render( renderer ) 30 | } 31 | 32 | material() { 33 | return new THREE.ShaderMaterial( { 34 | uniforms: { 35 | tDiffuse: { value: null } 36 | }, 37 | vertexShader: 38 | `varying vec2 vUv; 39 | void main() { 40 | vUv = uv; 41 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 42 | }`, 43 | fragmentShader: 44 | `uniform sampler2D tDiffuse; 45 | varying vec2 vUv; 46 | void main() { 47 | vec4 texel = texture2D( tDiffuse, vUv ); 48 | gl_FragColor = 1. - texel; 49 | }` 50 | } ) 51 | } 52 | } -------------------------------------------------------------------------------- /src/PixelatePass.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three" 2 | import { WebGLRenderer, WebGLRenderTarget } from "three" 3 | import { Pass, FullScreenQuad } from "three/examples/jsm/postprocessing/Pass" 4 | 5 | export default class PixelatePass extends Pass { 6 | 7 | fsQuad: FullScreenQuad 8 | resolution: THREE.Vector2 9 | 10 | constructor( resolution: THREE.Vector2 ) { 11 | super() 12 | this.resolution = resolution 13 | this.fsQuad = new FullScreenQuad( this.material() ) 14 | } 15 | 16 | render( 17 | renderer: WebGLRenderer, 18 | writeBuffer: WebGLRenderTarget, 19 | readBuffer: WebGLRenderTarget 20 | ) { 21 | // @ts-ignore 22 | const uniforms = this.fsQuad.material.uniforms 23 | uniforms.tDiffuse.value = readBuffer.texture 24 | if ( this.renderToScreen ) { 25 | renderer.setRenderTarget( null ) 26 | } else { 27 | renderer.setRenderTarget( writeBuffer ) 28 | if ( this.clear ) renderer.clear() 29 | } 30 | this.fsQuad.render( renderer ) 31 | } 32 | 33 | material() { 34 | return new THREE.ShaderMaterial( { 35 | uniforms: { 36 | tDiffuse: { value: null }, 37 | resolution: { 38 | value: new THREE.Vector4( 39 | this.resolution.x, 40 | this.resolution.y, 41 | 1 / this.resolution.x, 42 | 1 / this.resolution.y, 43 | ) 44 | } 45 | }, 46 | vertexShader: 47 | ` 48 | varying vec2 vUv; 49 | void main() { 50 | vUv = uv; 51 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 52 | } 53 | `, 54 | fragmentShader: 55 | ` 56 | uniform sampler2D tDiffuse; 57 | uniform vec4 resolution; 58 | varying vec2 vUv; 59 | void main() { 60 | vec2 iuv = (floor(resolution.xy * vUv) + .5) * resolution.zw; 61 | vec4 texel = texture2D( tDiffuse, iuv ); 62 | gl_FragColor = texel; 63 | } 64 | ` 65 | } ) 66 | } 67 | } -------------------------------------------------------------------------------- /src/RenderPixelatedPass.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three" 2 | import { Vector2, WebGLRenderer, WebGLRenderTarget } from "three" 3 | import { Pass, FullScreenQuad } from "three/examples/jsm/postprocessing/Pass" 4 | 5 | export default class RenderPixelatedPass extends Pass { 6 | 7 | fsQuad: FullScreenQuad 8 | resolution: THREE.Vector2 9 | scene: THREE.Scene 10 | camera: THREE.Camera 11 | rgbRenderTarget: WebGLRenderTarget 12 | normalRenderTarget: WebGLRenderTarget 13 | normalMaterial: THREE.Material 14 | 15 | constructor( resolution: THREE.Vector2, scene: THREE.Scene, camera: THREE.Camera ) { 16 | super() 17 | this.resolution = resolution 18 | this.fsQuad = new FullScreenQuad( this.material() ) 19 | this.scene = scene 20 | this.camera = camera 21 | 22 | this.rgbRenderTarget = pixelRenderTarget( resolution, THREE.RGBAFormat, true ) 23 | this.normalRenderTarget = pixelRenderTarget( resolution, THREE.RGBFormat, false ) 24 | 25 | this.normalMaterial = new THREE.MeshNormalMaterial() 26 | } 27 | 28 | render( 29 | renderer: WebGLRenderer, 30 | writeBuffer: WebGLRenderTarget 31 | ) { 32 | renderer.setRenderTarget( this.rgbRenderTarget ) 33 | renderer.render( this.scene, this.camera ) 34 | 35 | const overrideMaterial_old = this.scene.overrideMaterial 36 | renderer.setRenderTarget( this.normalRenderTarget ) 37 | this.scene.overrideMaterial = this.normalMaterial 38 | renderer.render( this.scene, this.camera ) 39 | this.scene.overrideMaterial = overrideMaterial_old 40 | 41 | // @ts-ignore 42 | const uniforms = this.fsQuad.material.uniforms 43 | uniforms.tDiffuse.value = this.rgbRenderTarget.texture 44 | uniforms.tDepth.value = this.rgbRenderTarget.depthTexture 45 | uniforms.tNormal.value = this.normalRenderTarget.texture 46 | 47 | if ( this.renderToScreen ) { 48 | renderer.setRenderTarget( null ) 49 | } else { 50 | renderer.setRenderTarget( writeBuffer ) 51 | if ( this.clear ) renderer.clear() 52 | } 53 | this.fsQuad.render( renderer ) 54 | } 55 | 56 | material() { 57 | return new THREE.ShaderMaterial( { 58 | uniforms: { 59 | tDiffuse: { value: null }, 60 | tDepth: { value: null }, 61 | tNormal: { value: null }, 62 | resolution: { 63 | value: new THREE.Vector4( 64 | this.resolution.x, 65 | this.resolution.y, 66 | 1 / this.resolution.x, 67 | 1 / this.resolution.y, 68 | ) 69 | } 70 | }, 71 | vertexShader: 72 | ` 73 | varying vec2 vUv; 74 | void main() { 75 | vUv = uv; 76 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 77 | } 78 | `, 79 | fragmentShader: 80 | ` 81 | uniform sampler2D tDiffuse; 82 | uniform sampler2D tDepth; 83 | uniform sampler2D tNormal; 84 | uniform vec4 resolution; 85 | varying vec2 vUv; 86 | 87 | float getDepth(int x, int y) { 88 | return texture2D( tDepth, vUv + vec2(x, y) * resolution.zw ).r; 89 | } 90 | 91 | vec3 getNormal(int x, int y) { 92 | return texture2D( tNormal, vUv + vec2(x, y) * resolution.zw ).rgb * 2.0 - 1.0; 93 | } 94 | 95 | float neighborNormalEdgeIndicator(int x, int y, float depth, vec3 normal) { 96 | float depthDiff = getDepth(x, y) - depth; 97 | 98 | // Edge pixels should yield to faces closer to the bias direction. 99 | vec3 normalEdgeBias = vec3(1., 1., 1.); // This should probably be a parameter. 100 | float normalDiff = dot(normal - getNormal(x, y), normalEdgeBias); 101 | float normalIndicator = clamp(smoothstep(-.01, .01, normalDiff), 0.0, 1.0); 102 | 103 | // Only the shallower pixel should detect the normal edge. 104 | float depthIndicator = clamp(sign(depthDiff * .25 + .0025), 0.0, 1.0); 105 | 106 | return distance(normal, getNormal(x, y)) * depthIndicator * normalIndicator; 107 | } 108 | 109 | float depthEdgeIndicator() { 110 | float depth = getDepth(0, 0); 111 | vec3 normal = getNormal(0, 0); 112 | float diff = 0.0; 113 | diff += clamp(getDepth(1, 0) - depth, 0.0, 1.0); 114 | diff += clamp(getDepth(-1, 0) - depth, 0.0, 1.0); 115 | diff += clamp(getDepth(0, 1) - depth, 0.0, 1.0); 116 | diff += clamp(getDepth(0, -1) - depth, 0.0, 1.0); 117 | return floor(smoothstep(0.01, 0.02, diff) * 2.) / 2.; 118 | } 119 | 120 | float normalEdgeIndicator() { 121 | float depth = getDepth(0, 0); 122 | vec3 normal = getNormal(0, 0); 123 | 124 | float indicator = 0.0; 125 | 126 | indicator += neighborNormalEdgeIndicator(0, -1, depth, normal); 127 | indicator += neighborNormalEdgeIndicator(0, 1, depth, normal); 128 | indicator += neighborNormalEdgeIndicator(-1, 0, depth, normal); 129 | indicator += neighborNormalEdgeIndicator(1, 0, depth, normal); 130 | 131 | return step(0.1, indicator); 132 | } 133 | 134 | float lum(vec4 color) { 135 | vec4 weights = vec4(.2126, .7152, .0722, .0); 136 | return dot(color, weights); 137 | } 138 | 139 | float smoothSign(float x, float radius) { 140 | return smoothstep(-radius, radius, x) * 2.0 - 1.0; 141 | } 142 | 143 | void main() { 144 | vec4 texel = texture2D( tDiffuse, vUv ); 145 | 146 | float tLum = lum(texel); 147 | // float normalEdgeCoefficient = (smoothSign(tLum - .3, .1) + .7) * .25; 148 | // float depthEdgeCoefficient = (smoothSign(tLum - .3, .1) + .7) * .3; 149 | float normalEdgeCoefficient = .3; 150 | float depthEdgeCoefficient = .4; 151 | 152 | float dei = depthEdgeIndicator(); 153 | float nei = normalEdgeIndicator(); 154 | 155 | float coefficient = dei > 0.0 ? (1.0 - depthEdgeCoefficient * dei) : (1.0 + normalEdgeCoefficient * nei); 156 | 157 | gl_FragColor = texel * coefficient; 158 | } 159 | ` 160 | } ) 161 | } 162 | } 163 | 164 | function pixelRenderTarget( resolution: THREE.Vector2, pixelFormat: THREE.PixelFormat, depthTexture: boolean ) { 165 | const renderTarget = new WebGLRenderTarget( 166 | resolution.x, resolution.y, 167 | !depthTexture ? 168 | undefined 169 | : { 170 | depthTexture: new THREE.DepthTexture( 171 | resolution.x, 172 | resolution.y 173 | ), 174 | depthBuffer: true 175 | } 176 | ) 177 | renderTarget.texture.format = pixelFormat 178 | renderTarget.texture.minFilter = THREE.NearestFilter 179 | renderTarget.texture.magFilter = THREE.NearestFilter 180 | renderTarget.texture.generateMipmaps = false 181 | renderTarget.stencilBuffer = false 182 | return renderTarget 183 | } 184 | -------------------------------------------------------------------------------- /src/assets/TileCrate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KodyJKing/hello-threejs/c47979c4025f494d403574bbfb08da87de58f94c/src/assets/TileCrate.png -------------------------------------------------------------------------------- /src/assets/mech.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KodyJKing/hello-threejs/c47979c4025f494d403574bbfb08da87de58f94c/src/assets/mech.fbx -------------------------------------------------------------------------------- /src/assets/mech2.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KodyJKing/hello-threejs/c47979c4025f494d403574bbfb08da87de58f94c/src/assets/mech2.fbx -------------------------------------------------------------------------------- /src/assets/smoothTest1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KodyJKing/hello-threejs/c47979c4025f494d403574bbfb08da87de58f94c/src/assets/smoothTest1.fbx -------------------------------------------------------------------------------- /src/assets/smoothTest2.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KodyJKing/hello-threejs/c47979c4025f494d403574bbfb08da87de58f94c/src/assets/smoothTest2.fbx -------------------------------------------------------------------------------- /src/assets/smoothTest3.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KodyJKing/hello-threejs/c47979c4025f494d403574bbfb08da87de58f94c/src/assets/smoothTest3.fbx -------------------------------------------------------------------------------- /src/assets/warningStripes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KodyJKing/hello-threejs/c47979c4025f494d403574bbfb08da87de58f94c/src/assets/warningStripes.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three" 2 | import { GreaterEqualDepth, Vector2 } from "three" 3 | 4 | import { MapControls, OrbitControls } from "three/examples/jsm/controls/OrbitControls" 5 | import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' 6 | 7 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer' 8 | import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass' 9 | import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass' 10 | import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass' 11 | 12 | 13 | import HelloWorldPass from "./HelloWorldPass" 14 | import RenderPixelatedPass from "./RenderPixelatedPass" 15 | import PixelatePass from "./PixelatePass" 16 | 17 | import { stopGoEased } from "./math" 18 | 19 | import warningStipesURL from "./assets/warningStripes.png" 20 | import crateURL from "./assets/TileCrate.png" 21 | import mechURL from "./assets/mech.fbx" 22 | 23 | let camera: THREE.Camera, scene: THREE.Scene, renderer: THREE.WebGLRenderer, composer: EffectComposer 24 | let controls: OrbitControls 25 | let crystalMesh: THREE.Mesh, mech: THREE.Object3D 26 | 27 | init() 28 | animate() 29 | 30 | function init() { 31 | 32 | let screenResolution = new Vector2( window.innerWidth, window.innerHeight ) 33 | let renderResolution = screenResolution.clone().divideScalar( 6 ) 34 | renderResolution.x |= 0 35 | renderResolution.y |= 0 36 | let aspectRatio = screenResolution.x / screenResolution.y 37 | 38 | camera = new THREE.OrthographicCamera( -aspectRatio, aspectRatio, 1, -1, 0.1, 10 ) 39 | scene = new THREE.Scene() 40 | scene.background = new THREE.Color( 0x151729 ) 41 | // scene.background = new THREE.Color( 0xffffff ) 42 | 43 | // Renderer 44 | renderer = new THREE.WebGLRenderer( { antialias: false } ) 45 | // renderer.toneMapping = THREE.ACESFilmicToneMapping 46 | // renderer.toneMappingExposure = .75 47 | renderer.shadowMap.enabled = true 48 | renderer.setSize( screenResolution.x, screenResolution.y ) 49 | document.body.appendChild( renderer.domElement ) 50 | 51 | composer = new EffectComposer( renderer ) 52 | // composer.addPass( new RenderPass( scene, camera ) ) 53 | composer.addPass( new RenderPixelatedPass( renderResolution, scene, camera ) ) 54 | let bloomPass = new UnrealBloomPass( screenResolution, .4, .1, .9 ) 55 | composer.addPass( bloomPass ) 56 | composer.addPass( new PixelatePass( renderResolution ) ) 57 | 58 | controls = new OrbitControls( camera, renderer.domElement ) 59 | controls.target.set( 0, 0, 0 ) 60 | camera.position.z = 2 61 | camera.position.y = 2 * Math.tan( Math.PI / 6 ) 62 | controls.update() 63 | // controls.minPolarAngle = controls.maxPolarAngle = controls.getPolarAngle() 64 | 65 | const texLoader = new THREE.TextureLoader() 66 | const tex_crate = pixelTex( texLoader.load( crateURL ) ) 67 | const tex_warningStripes = pixelTex( texLoader.load( warningStipesURL ) ) 68 | const tex_checker = pixelTex( texLoader.load( "https://threejsfundamentals.org/threejs/resources/images/checker.png" ) ) 69 | const tex_checker2 = pixelTex( texLoader.load( "https://threejsfundamentals.org/threejs/resources/images/checker.png" ) ) 70 | tex_checker.repeat.set( 3, 3 ) 71 | tex_checker2.repeat.set( 1.5, 1.5 ) 72 | 73 | // Geometry 74 | // { 75 | // const fbxLoader = new FBXLoader() 76 | // let mechMaterial = new THREE.MeshPhongMaterial( { 77 | // // map: tex_checker2, 78 | // specular: 0xffffff, 79 | // shininess: 100 80 | // } ) 81 | // fbxLoader.load( mechURL, obj => { 82 | // // obj.scale.set( .005, .005, .005 ) 83 | // obj.scale.set( .001, .001, .001 ) 84 | // obj.traverse( child => { 85 | // // @ts-ignore 86 | // if ( child instanceof THREE.Mesh ) { 87 | // child.castShadow = true 88 | // child.receiveShadow = true 89 | // child.material = mechMaterial 90 | // } 91 | // } ) 92 | // console.log( obj ) 93 | // mech = obj 94 | // scene.add( obj ) 95 | // } ) 96 | // } 97 | { 98 | // let boxMaterial = new THREE.MeshNormalMaterial() 99 | let boxMaterial = new THREE.MeshPhongMaterial( { map: tex_checker2 } ) 100 | // let boxMaterial = new THREE.MeshPhongMaterial() 101 | function addBox( boxSideLength: number, x: number, z: number, rotation: number ) { 102 | let mesh = new THREE.Mesh( new THREE.BoxGeometry( boxSideLength, boxSideLength, boxSideLength ), boxMaterial ) 103 | mesh.castShadow = true 104 | mesh.receiveShadow = true 105 | mesh.rotation.y = rotation 106 | mesh.position.y = boxSideLength / 2 107 | mesh.position.set( x, boxSideLength / 2 + .0001, z ) 108 | scene.add( mesh ) 109 | return mesh 110 | } 111 | addBox( .4, 0, 0, Math.PI / 4 ) 112 | addBox( .2, -.4, -.15, Math.PI / 4 ) 113 | } 114 | { 115 | const planeSideLength = 2 116 | let planeMesh = new THREE.Mesh( 117 | new THREE.PlaneGeometry( planeSideLength, planeSideLength ), 118 | new THREE.MeshPhongMaterial( { 119 | map: tex_checker, 120 | // side: THREE.DoubleSide 121 | } ) 122 | // new THREE.MeshPhongMaterial( { side: THREE.DoubleSide } ) 123 | ) 124 | planeMesh.receiveShadow = true 125 | planeMesh.rotation.x = -Math.PI / 2 126 | scene.add( planeMesh ) 127 | } 128 | { 129 | const radius = .2 130 | // const geometry = new THREE.DodecahedronGeometry( radius ) 131 | const geometry = new THREE.IcosahedronGeometry( radius ) 132 | crystalMesh = new THREE.Mesh( 133 | geometry, 134 | new THREE.MeshPhongMaterial( { 135 | color: 0x2379cf, 136 | emissive: 0x143542, 137 | shininess: 100, 138 | specular: 0xffffff, 139 | // opacity: 0.5 140 | } ) 141 | // new THREE.MeshNormalMaterial() 142 | ) 143 | crystalMesh.receiveShadow = true 144 | crystalMesh.castShadow = true 145 | scene.add( crystalMesh ) 146 | } 147 | 148 | // Lights 149 | scene.add( new THREE.AmbientLight( 0x2d3645, 1.5 ) ) 150 | { 151 | let directionalLight = new THREE.DirectionalLight( 0xfffc9c, .5 ) 152 | directionalLight.position.set( 100, 100, 100 ) 153 | directionalLight.castShadow = true 154 | // directionalLight.shadow.radius = 0 155 | directionalLight.shadow.mapSize.set( 2048, 2048 ) 156 | scene.add( directionalLight ) 157 | } 158 | { 159 | let spotLight = new THREE.SpotLight( 0xff8800, 1, 10, Math.PI / 16, .02, 2 ) 160 | // let spotLight = new THREE.SpotLight( 0xff8800, 1, 10, Math.PI / 16, 0, 2 ) 161 | spotLight.position.set( 2, 2, 0 ) 162 | let target = spotLight.target //= new THREE.Object3D() 163 | scene.add( target ) 164 | target.position.set( 0, 0, 0 ) 165 | spotLight.castShadow = true 166 | scene.add( spotLight ) 167 | // spotLight.shadow.radius = 0 168 | } 169 | } 170 | 171 | function animate() { 172 | requestAnimationFrame( animate ) 173 | let t = performance.now() / 1000 174 | 175 | let mat = ( crystalMesh.material as THREE.MeshPhongMaterial ) 176 | mat.emissiveIntensity = Math.sin( t * 3 ) * .5 + .5 177 | crystalMesh.position.y = .7 + Math.sin( t * 2 ) * .05 178 | // crystalMesh.rotation.y = stopGoEased( t, 3, 4 ) * Math.PI / 2 179 | crystalMesh.rotation.y = stopGoEased( t, 2, 4 ) * 2 * Math.PI 180 | 181 | // if ( mech ) 182 | // mech.rotation.y = Math.floor( t * 8 ) * Math.PI / 32 183 | composer.render() 184 | } 185 | 186 | function pixelTex( tex: THREE.Texture ) { 187 | tex.minFilter = THREE.NearestFilter 188 | tex.magFilter = THREE.NearestFilter 189 | tex.generateMipmaps = false 190 | tex.wrapS = THREE.RepeatWrapping 191 | tex.wrapT = THREE.RepeatWrapping 192 | return tex 193 | } 194 | -------------------------------------------------------------------------------- /src/math.ts: -------------------------------------------------------------------------------- 1 | export function easeInOutCubic( x ) { 2 | return x ** 2 * 3 - x ** 3 * 2 3 | } 4 | 5 | export function clamp( x, min, max ) { 6 | return Math.min( max, Math.max( min, x ) ) 7 | } 8 | 9 | export function sawtooth( x, radius = 1, height = 1 ) { 10 | x = Math.abs( x ) / radius 11 | let rising = x % 2 12 | let falling = Math.max( 0, rising * 2 - 2 ) 13 | return ( rising - falling ) * height 14 | } 15 | 16 | export function linearStep( x, edge0, edge1 ) { 17 | let w = edge1 - edge0 18 | let m = 1 / w // slope with a rise of 1 19 | let y0 = -m * edge0 20 | return clamp( y0 + m * x, 0, 1 ) 21 | } 22 | 23 | export function stopGo( x, downtime, period ) { 24 | let cycle = ( x / period ) | 0 25 | let tween = x - cycle * period 26 | let linStep = linearStep( tween, downtime, period ) 27 | return cycle + linStep 28 | } 29 | 30 | export function stopGoEased( x, downtime, period ) { 31 | let cycle = ( x / period ) | 0 32 | let tween = x - cycle * period 33 | let linStep = easeInOutCubic( linearStep( tween, downtime, period ) ) 34 | return cycle + linStep 35 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src" 4 | ], 5 | "compilerOptions": { 6 | "target": "ESNext", 7 | "module": "ES2020", 8 | "declaration": true, 9 | "outDir": "lib", 10 | "rootDir": "src", 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "allowJs": true, 14 | "allowSyntheticDefaultImports": true, 15 | "experimentalDecorators": true, 16 | "esModuleInterop": true, 17 | "preserveWatchOutput": true 18 | } 19 | } --------------------------------------------------------------------------------