[READ-ONLY] Mirror of https://github.com/flo-bit/tiny-creature. three.js journey #16 - tamagotchi flo-bit.dev/tiny-creature/
0

Configure Feed

Select the types of activity you want to include in your feed.

some cleanup

Florian (Feb 5, 2025, 10:07 PM +0100) c617f873 1c66b318

+234 -700
+72 -112
src/lib/Creature.svelte
··· 5 5 import { WiggleBone } from 'wiggle'; 6 6 import { onMount } from 'svelte'; 7 7 import { createFaceTexture, createFurTexture, updateFace } from './face/face'; 8 - import { T, useTask, useThrelte } from '@threlte/core'; 8 + import { T, useTask } from '@threlte/core'; 9 9 10 10 let rootBone: THREE.Bone; 11 11 const wiggleBones: WiggleBone[] = []; ··· 18 18 color, 19 19 float, 20 20 Fn, 21 - If, 22 21 mix, 23 22 mx_noise_float, 24 23 normalGeometry, 25 - normalLocal, 26 - normalView, 27 - oscSine, 28 24 positionGeometry, 29 - positionLocal, 30 25 select, 31 26 texture, 32 - uniform, 33 - uv, 34 27 vec3 35 28 } from 'three/tsl'; 36 29 37 - const uniforms = []; 38 - 39 - const mouse = new THREE.Vector2(); 30 + function jump() { 31 + ySpeed = 5; 32 + } 40 33 41 34 onMount(async () => { 42 - // add mouse movement 43 - window.addEventListener('mousemove', (event) => { 44 - // set to be between 0 and 1 45 - mouse.set(event.clientX / window.innerWidth, event.clientY / window.innerHeight); 35 + // jump on touch, click and space 36 + window.addEventListener('touchstart', (event) => { 37 + jump(); 46 38 }); 47 - 48 - // jujmp on space 39 + window.addEventListener('click', (event) => { 40 + jump(); 41 + }); 49 42 window.addEventListener('keydown', (event) => { 50 43 if (event.key === ' ') { 51 - ySpeed = 5; 44 + jump(); 52 45 } 53 46 }); 54 47 ··· 77 70 let scale = 1.4; 78 71 creature.scale.set(scale, scale, scale); 79 72 80 - if (mesh instanceof THREE.Mesh) { 81 - // mesh.material = new THREE.MeshStandardMaterial({ 82 - // map: canvasTexture, 83 - // }); 84 - 85 - // const detail = texture( canvasTexture, uv().mul( 10 ) ); 86 - 87 - const material = new THREE.MeshStandardNodeMaterial(); 88 - // material.colorNode = positionGeometry.sub(positionLocal); //texture(canvasTexture); 89 - // material.colorNode = color( 0x0066ff ); 90 - material.colorNode = texture(canvasTexture); 91 - 92 - // const limitPosition = Fn(({ position }) => { 93 - // const limit = oscSine(); 94 - 95 - // // Convert to variable using `.toVar()` to be able to use assignments. 96 - // const result = position.toVec3().toVar(); 73 + if (!(mesh instanceof THREE.SkinnedMesh)) return; 97 74 98 - // result.y = select( result.y.greaterThan( limit ), limit, result.y ); 75 + // add face and fur 76 + const material = new THREE.MeshStandardNodeMaterial({ 77 + roughness: 0.5 78 + }); 79 + material.colorNode = texture(canvasTexture); 99 80 100 - // return result; 101 - // }); 81 + material.bumpMap = canvasTexture; 102 82 103 - // material.positionNode = limitPosition({ position: positionLocal }); 83 + mesh.material = material; 104 84 105 - mesh.material = material; 106 - 107 - const nodes = []; 108 - 109 - let total = 16; 110 - for (let i = 0; i < total; i++) { 111 - const material = new THREE.MeshStandardNodeMaterial({ 112 - transparent: true, 113 - opacity: 1, 114 - side: THREE.DoubleSide 115 - }); 85 + const furMeshes = []; 116 86 117 - const movement = uniform(vec3(0, 0, 0)); 87 + let total = 16; 88 + for (let i = 0; i < total; i++) { 89 + const material = new THREE.MeshStandardNodeMaterial({ 90 + transparent: true, 91 + opacity: 1, 92 + side: THREE.DoubleSide 93 + }); 118 94 119 - uniforms.push(movement); 95 + material.positionNode = positionGeometry 96 + .add(normalGeometry.mul(i * 0.007)) 97 + .add(vec3(0, -0.002, 0).mul(i)); 120 98 121 - material.positionNode = positionGeometry 122 - .add( 123 - normalGeometry.mul( 124 - float(i) 125 - .mul(0.01) 126 - .sub(float(1).sub(texture(furTexture).a).mul(0.2)) 127 - ) 128 - ) 129 - .add(movement.add(vec3(0, -0.003, 0)).mul(i * 1)); 99 + material.colorNode = Fn(() => { 100 + const t = texture(furTexture).toVar(); 130 101 131 - material.colorNode = mix( 102 + const c = mix( 132 103 color(0xec4899), 133 104 color(0xfb923c), 134 105 mx_noise_float(positionGeometry.mul(10).mul(0.5).add(0.5)) 135 - ).mul((i / total) * 0.8 + 0.2); 106 + ); 136 107 137 - material.alphaTestNode = float(0.5); 108 + const s = select(t.g.lessThan(0.01).and(t.b.lessThan(0.01)), c, t.rgb); 138 109 139 - material.opacityNode = select( 140 - mx_noise_float(positionGeometry.mul(100)).greaterThan((i / total) * 2 - 1), 141 - 1.0, 142 - 0.0 110 + return s.mul( 111 + float(i / total) 112 + .add(t.a.mul(-1).add(1)) 113 + .mul(0.8) 114 + .add(0.2) 143 115 ); 116 + })(); 144 117 145 - const grass = mesh.clone(); 146 - grass.material = material; 147 - 148 - // grass.position.y = i * 0.01; 149 - nodes.push(grass); 150 - } 118 + material.alphaTestNode = float(0.5); 151 119 152 - mesh.add(...nodes); 153 - } 120 + material.opacityNode = Fn(() => { 121 + const t = texture(furTexture).toVar(); 154 122 155 - // obj.rotation.y = -Math.PI / 2; 156 - // obj.lookAt(camera.position); 123 + return select( 124 + mx_noise_float(positionGeometry.mul(float(200).add(t.a.mul(-100)))).greaterThan( 125 + float((i / total) * 2 - 1).add(t.a.mul(-2).add(2)) 126 + ), 127 + 1.0, 128 + 0.0 129 + ); 130 + })(); 157 131 158 - if (mesh instanceof THREE.SkinnedMesh) { 159 - mesh.skeleton.bones.forEach((bone) => { 160 - if (!(bone.parent instanceof THREE.Bone) || !bone.parent?.isBone) { 161 - rootBone = bone; 132 + const fur = mesh.clone(); 133 + fur.material = material; 162 134 163 - console.log('found root bone', rootBone); 135 + furMeshes.push(fur); 136 + } 164 137 165 - // rootBone.rotation.y = -Math.PI / 2; 166 - } else { 167 - const wiggleBone = new WiggleBone(bone, { 168 - velocity: 0.5 169 - }); 170 - wiggleBones.push(wiggleBone); 138 + mesh.add(...furMeshes); 171 139 172 - console.log('found wiggle bone', wiggleBone); 173 - } 174 - }); 175 - } 140 + // add wiggle bones 141 + mesh.skeleton.bones.forEach((bone) => { 142 + if (!(bone.parent instanceof THREE.Bone) || !bone.parent?.isBone) { 143 + rootBone = bone; 144 + } else { 145 + const wiggleBone = new WiggleBone(bone, { 146 + velocity: 0.4 147 + }); 148 + wiggleBones.push(wiggleBone); 149 + } 150 + }); 176 151 }); 177 152 }); 178 153 179 - let lastPosition = new THREE.Vector3(); 180 154 let total = 0; 181 155 182 156 let ySpeed = 0; 157 + 183 158 useTask((dt) => { 184 159 total += dt; 185 160 wiggleBones.forEach((wiggleBone) => { ··· 192 167 } 193 168 194 169 if (furTexture) { 195 - // furTexture.needsUpdate = true; 170 + furTexture.needsUpdate = true; 196 171 } 197 172 198 173 if (rootBone) { 199 - rootBone.position.z = Math.sin(total * 2) * 0.5; //-(mouse.x - 0.5) * 0.8 + rootBone.position.z * 0.2; 174 + rootBone.position.x = Math.sin(total * 2) * 0.5; 200 175 201 176 rootBone.position.y += ySpeed * dt; 202 - rootBone.position.y = Math.max(rootBone.position.y, -0.5); 177 + rootBone.position.y = Math.max(rootBone.position.y, -1); 203 178 204 179 ySpeed -= 10 * dt; 205 - // uniforms.forEach((uniform) => { 206 - // uniform.value.set( 207 - // (lastPosition.x - rootBone.position.x) * dt, 208 - // (lastPosition.y - rootBone.position.y) * dt, 209 - // (lastPosition.z - rootBone.position.z) * dt 210 - // ); 211 - // }); 212 - 213 - // console.log( 214 - // (lastPosition.x - rootBone.position.x) * dt, 215 - // (lastPosition.y - rootBone.position.y) * dt, 216 - // (lastPosition.z - rootBone.position.z) * dt 217 - // ); 218 - 219 - lastPosition.copy(rootBone.position); 220 180 } 221 181 }); 222 182 </script>
-34
src/lib/Grass.svelte
··· 1 - <script lang="ts"> 2 - import { GLTFLoader } from 'three/examples/jsm/Addons.js'; 3 - import * as THREE from 'three/webgpu'; 4 - 5 - import { onMount } from 'svelte'; 6 - import { T, useTask, useThrelte } from '@threlte/core'; 7 - import { Fn, If, normalView, oscSine, positionGeometry, positionLocal, select, texture, uv, mx_noise_float, color, normalLocal } from 'three/tsl'; 8 - 9 - 10 - const nodes = []; 11 - 12 - let total = 32; 13 - for(let i = 0; i < total; i++) { 14 - const material = new THREE.MeshBasicNodeMaterial({transparent: true, opacity: 1}); 15 - const geometry = new THREE.PlaneGeometry(10, 10); 16 - 17 - material.positionNode = positionLocal.add(normalLocal.mul(i * 0.01)); 18 - 19 - material.colorNode = color(0.2, 0.8, 0.2).mul((i / total) * 0.8 + 0.2); 20 - 21 - material.opacityNode = select( mx_noise_float(uv().mul(500)).greaterThan( (i / total) * 2 - 1 ), 1.0, 0.0 ); 22 - 23 - const grass = new THREE.Mesh(geometry, material); 24 - 25 - grass.rotation.x = -Math.PI / 2; 26 - // grass.position.y = i * 0.01; 27 - nodes.push(grass); 28 - } 29 - 30 - </script> 31 - 32 - {#each nodes as node} 33 - <T is={node} /> 34 - {/each}
-180
src/lib/Room.svelte
··· 1 - <!-- 2 - Auto-generated by: https://github.com/threlte/threlte/tree/main/packages/gltf 3 - Command: npx @threlte/gltf@3.0.0 room.glb -t -s 4 - --> 5 - 6 - <script lang="ts"> 7 - import type * as THREE from 'three' 8 - 9 - import type { Snippet } from 'svelte' 10 - import { T, type Props } from '@threlte/core' 11 - import { useGltf } from '@threlte/extras' 12 - 13 - let { 14 - fallback, 15 - error, 16 - children, 17 - ref = $bindable(), 18 - ...props 19 - }: Props<THREE.Group> & { 20 - ref?: THREE.Group 21 - children?: Snippet<[{ ref: THREE.Group }]> 22 - fallback?: Snippet 23 - error?: Snippet<[{ error: Error }]> 24 - } = $props() 25 - 26 - type GLTFResult = { 27 - nodes: { 28 - Cube: THREE.Mesh 29 - couch001: THREE.Mesh 30 - armchair001: THREE.Mesh 31 - cabinet_medium_decorated001: THREE.Mesh 32 - table_small001: THREE.Mesh 33 - lamp_desk001: THREE.Mesh 34 - rug_rectangle_stripes_A001: THREE.Mesh 35 - shelf_B_small_decorated001: THREE.Mesh 36 - pictureframe_large_A001: THREE.Mesh 37 - pictureframe_medium001: THREE.Mesh 38 - pictureframe_large_B001: THREE.Mesh 39 - lamp_standing001: THREE.Mesh 40 - lamp_standing002: THREE.Mesh 41 - } 42 - materials: { 43 - furniture_texture: THREE.MeshStandardMaterial 44 - } 45 - } 46 - 47 - const gltf = useGltf<GLTFResult>('/tiny-creature/room.glb') 48 - </script> 49 - 50 - <T.Group 51 - bind:ref 52 - dispose={false} 53 - {...props} 54 - > 55 - {#await gltf} 56 - {@render fallback?.()} 57 - {:then gltf} 58 - <T.PointLight 59 - intensity={10} 60 - decay={2} 61 - position={[2.65, 2.17, -2.57]} 62 - rotation={[-Math.PI / 2, 0, 0]} 63 - color="#ffaa44" 64 - /> 65 - <T.SpotLight 66 - intensity={50} 67 - angle={Math.PI / 9} 68 - penumbra={0.15} 69 - decay={2} 70 - position={[2.34, 2.05, 1.93]} 71 - rotation={[-1.48, 0.31, 0.07]} 72 - color="#ffaa44" 73 - /> 74 - <T.Mesh 75 - castShadow 76 - receiveShadow 77 - geometry={gltf.nodes.Cube.geometry} 78 - material={gltf.nodes.Cube.material} 79 - position={[0, 1.56, 0]} 80 - scale={[0.78, 0.39, 0.78]} 81 - /> 82 - <T.Mesh 83 - castShadow 84 - receiveShadow 85 - geometry={gltf.nodes.couch001.geometry} 86 - material={gltf.materials.furniture_texture} 87 - position={[2.33, 0, -0.53]} 88 - rotation={[0, -1.53, 0]} 89 - /> 90 - <T.Mesh 91 - castShadow 92 - receiveShadow 93 - geometry={gltf.nodes.armchair001.geometry} 94 - material={gltf.materials.furniture_texture} 95 - position={[0.51, 0, -2.45]} 96 - rotation={[0, 0.32, 0]} 97 - /> 98 - <T.Mesh 99 - castShadow 100 - receiveShadow 101 - geometry={gltf.nodes.cabinet_medium_decorated001.geometry} 102 - material={gltf.materials.furniture_texture} 103 - position={[-1.85, 0, -2.7]} 104 - /> 105 - <T.Mesh 106 - castShadow 107 - receiveShadow 108 - geometry={gltf.nodes.table_small001.geometry} 109 - material={gltf.materials.furniture_texture} 110 - position={[2.72, 0, 2.01]} 111 - /> 112 - <T.Mesh 113 - castShadow 114 - receiveShadow 115 - geometry={gltf.nodes.lamp_desk001.geometry} 116 - material={gltf.materials.furniture_texture} 117 - position={[2.65, 0.97, 2.04]} 118 - rotation={[Math.PI, -1.17, Math.PI]} 119 - /> 120 - <T.Mesh 121 - castShadow 122 - receiveShadow 123 - geometry={gltf.nodes.rug_rectangle_stripes_A001.geometry} 124 - material={gltf.materials.furniture_texture} 125 - position={[-0.87, 0, 1.06]} 126 - rotation={[0, -0.64, 0]} 127 - /> 128 - <T.Mesh 129 - castShadow 130 - receiveShadow 131 - geometry={gltf.nodes.shelf_B_small_decorated001.geometry} 132 - material={gltf.materials.furniture_texture} 133 - position={[3.14, 2.25, 2.35]} 134 - rotation={[0, -Math.PI / 2, 0]} 135 - /> 136 - <T.Mesh 137 - castShadow 138 - receiveShadow 139 - geometry={gltf.nodes.pictureframe_large_A001.geometry} 140 - material={gltf.materials.furniture_texture} 141 - position={[-2.49, 2.38, -3.14]} 142 - /> 143 - <T.Mesh 144 - castShadow 145 - receiveShadow 146 - geometry={gltf.nodes.pictureframe_medium001.geometry} 147 - material={gltf.materials.furniture_texture} 148 - position={[-1.15, 2.57, -3.13]} 149 - /> 150 - <T.Mesh 151 - castShadow 152 - receiveShadow 153 - geometry={gltf.nodes.pictureframe_large_B001.geometry} 154 - material={gltf.materials.furniture_texture} 155 - position={[3.12, 2.41, -0.07]} 156 - rotation={[0, -Math.PI / 2, 0]} 157 - /> 158 - <T.Mesh 159 - castShadow 160 - receiveShadow 161 - geometry={gltf.nodes.lamp_standing001.geometry} 162 - material={gltf.materials.furniture_texture} 163 - position={[2.64, 0, -2.58]} 164 - /> 165 - <T.Mesh 166 - castShadow 167 - receiveShadow 168 - geometry={gltf.nodes.lamp_standing002.geometry} 169 - material={gltf.materials.furniture_texture.clone()} 170 - position={[2.64, 0, -2.58]} 171 - material.color="#ffaa44" 172 - material.emissive="#ffaa44" 173 - material.emissiveIntensity={3} 174 - /> 175 - {:catch err} 176 - {@render error?.({ error: err })} 177 - {/await} 178 - 179 - {@render children?.({ ref })} 180 - </T.Group>
+15 -58
src/lib/Scene.svelte
··· 1 1 <script lang="ts"> 2 2 import { T, useStage, useTask, useThrelte } from '@threlte/core'; 3 + import { Environment, OrbitControls } from '@threlte/extras'; 4 + import { onMount } from 'svelte'; 5 + import { ACESFilmicToneMapping } from 'three'; 6 + import Stats from 'stats.js'; 7 + 3 8 import Creature from './Creature.svelte'; 4 - import { 5 - Environment, 6 - OrbitControls, 7 - Sky, 8 - TransformControls, 9 - VirtualEnvironment 10 - } from '@threlte/extras'; 11 - import Room from './Room.svelte'; 12 - import { DoubleSide } from 'three'; 13 - import { onMount } from 'svelte'; 14 - import * as THREE from 'three'; 15 - import Grass from './Grass.svelte'; 16 - let debug = $state(false); 17 9 18 10 const { renderer } = useThrelte(); 19 - import Stats from 'stats.js'; 11 + 20 12 var stats = new Stats(); 21 13 22 14 onMount(() => { 23 - renderer.toneMapping = THREE.ACESFilmicToneMapping; 15 + renderer.toneMapping = ACESFilmicToneMapping; 24 16 renderer.toneMappingExposure = 0.4; 25 17 26 - stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom 18 + stats.showPanel(0); 27 19 document.body.appendChild(stats.dom); 28 20 }); 29 21 30 - useTask((delta) => { 31 - stats.begin(); 32 - }); 22 + 33 23 const { renderStage } = useThrelte(); 34 24 35 25 const afterRenderStage = useStage('after-render', { 36 26 after: renderStage 37 27 }); 28 + 29 + useTask(() => { 30 + stats.begin(); 31 + }); 38 32 useTask( 39 - (delta) => { 33 + () => { 40 34 stats.end(); 41 35 }, 42 36 { stage: afterRenderStage } 43 37 ); 44 38 </script> 45 39 46 - <T.PerspectiveCamera makeDefault position={[5, 1, 0]}> 40 + <T.PerspectiveCamera makeDefault position={[0, 0, 8]}> 47 41 <OrbitControls /> 48 42 </T.PerspectiveCamera> 49 43 50 44 <Creature /> 51 45 52 46 <Environment isBackground={false} url={'/tiny-creature/workshop.hdr'} /> 53 - 54 - <!-- <Grass /> --> 55 - 56 - <!-- <Room /> --> 57 - 58 - {#snippet lightformer( 59 - color: string, 60 - shape: 'circle' | 'plane', 61 - size: number, 62 - position: [number, number, number], 63 - visible: boolean 64 - )} 65 - <T.Group {position}> 66 - {#snippet children({ ref })} 67 - {#if visible} 68 - <TransformControls object={ref} /> 69 - {/if} 70 - <T.Mesh lookAt={[0, 0, 0]}> 71 - {#if shape === 'circle'} 72 - <T.CircleGeometry args={[size / 2]} /> 73 - {:else} 74 - <T.PlaneGeometry args={[size, size]} /> 75 - {/if} 76 - <T.MeshBasicMaterial {color} side={DoubleSide} /> 77 - </T.Mesh> 78 - {/snippet} 79 - </T.Group> 80 - {/snippet} 81 - 82 - <!-- <Sky /> --> 83 - <VirtualEnvironment visible={debug}> 84 - {@render lightformer('#FFfFfF', 'plane', 20, [0, 0, -20], debug)} 85 - {@render lightformer('#FFD0CB', 'circle', 5, [0, 5, 0], debug)} 86 - {@render lightformer('#2223FF', 'plane', 8, [-3, 0, 4], debug)} 87 - 88 - <Room /> 89 - </VirtualEnvironment>
+40 -50
src/lib/face/eye.ts
··· 59 59 targetY: number = 0; 60 60 61 61 constructor(opts: Partial<EyeOptions>) { 62 - console.log("Eye", opts); 63 - this.container = new PIXI.Container(); 62 + console.log('Eye', opts); 63 + this.container = new PIXI.Container(); 64 64 65 - this.eyeContainer = new PIXI.Container(); 65 + this.eyeContainer = new PIXI.Container(); 66 66 67 - this.container.position.set(opts.x ?? 0, opts.y ?? 0); 67 + this.container.position.set(opts.x ?? 0, opts.y ?? 0); 68 68 69 - this.size = opts.size ?? 1; 69 + this.size = opts.size ?? 1; 70 70 71 - if (opts.blinkStart) { 72 - this.blinkTimer = opts.blinkStart; 73 - } 74 - if (opts.blinkTimer) { 75 - this.blinkTimer = opts.blinkTimer; 76 - } 71 + if (opts.blinkStart) { 72 + this.blinkTimer = opts.blinkStart; 73 + } 74 + if (opts.blinkTimer) { 75 + this.blinkTimer = opts.blinkTimer; 76 + } 77 77 78 - this._x = new AnimatedValue(); 79 - this._y = new AnimatedValue(); 78 + this._x = new AnimatedValue(); 79 + this._y = new AnimatedValue(); 80 80 81 - this.container.addChild(this.eyeContainer); 81 + this.container.addChild(this.eyeContainer); 82 82 83 - if (opts.container) opts.container.addChild(this.container); 83 + if (opts.container) opts.container.addChild(this.container); 84 84 85 - this.base = new PIXI.Graphics() 86 - .ellipse(0, 0, this.size, this.size * 0.9) 87 - .fill(opts.eyeColor ?? 0xffffff); 88 - this.base.alpha = 1; 89 - this.eyeContainer.addChild(this.base); 85 + this.base = new PIXI.Graphics() 86 + .ellipse(0, 0, this.size, this.size * 0.9) 87 + .fill(opts.eyeColor ?? 0xffffff); 88 + this.base.alpha = 1; 89 + this.eyeContainer.addChild(this.base); 90 90 91 - this.pupil = new PIXI.Graphics() 92 - .circle(0, 0, this.size * 0.6) 93 - .fill(opts.pupilColor ?? 0x000000) 94 - .circle(this.size * 0.3, this.size * 0.3, 0.2 * this.size) 95 - .fill({ color: 0xffffff, alpha: 0.7 }); 96 - this.eyeContainer.addChild(this.pupil); 91 + this.pupil = new PIXI.Graphics() 92 + .circle(0, 0, this.size * 0.6) 93 + .fill(opts.pupilColor ?? 0x000000) 94 + .circle(this.size * 0.3, this.size * 0.3, 0.2 * this.size) 95 + .fill({ color: 0xffffff, alpha: 0.7 }); 96 + this.eyeContainer.addChild(this.pupil); 97 97 98 - this.mask = new PIXI.Graphics() 99 - .ellipse(0, 0, this.size, this.size * 0.9) 100 - .fill(0xffffff); 101 - this.eyeContainer.addChild(this.mask); 102 - this.eyeContainer.mask = this.mask; 98 + this.mask = new PIXI.Graphics().ellipse(0, 0, this.size, this.size * 0.9).fill(0xffffff); 99 + this.eyeContainer.addChild(this.mask); 100 + this.eyeContainer.mask = this.mask; 103 101 104 - const eyebrowWidth = opts?.eyebrow?.width ?? 2; 102 + const eyebrowWidth = opts?.eyebrow?.width ?? 2; 105 103 const eyebrowCurve = opts?.eyebrow?.curve ?? 0.2; 106 104 107 - const eyebrow = new PIXI.Graphics() 108 - .moveTo((-eyebrowWidth / 2) * this.size, 0) 109 - .quadraticCurveTo( 110 - 0, 111 - eyebrowCurve * this.size, 112 - (eyebrowWidth / 2) * this.size, 113 - 0, 114 - ) 115 - .stroke({ 116 - color: opts.eyebrow?.color ?? 0, 117 - width: opts.eyebrow?.width ?? this.size * 0.2, 118 - }); 119 - eyebrow.position.set( 120 - opts.eyebrow?.x ?? 0, 121 - (opts.eyebrow?.y ?? 0.9) * this.size, 122 - ); 105 + const eyebrow = new PIXI.Graphics() 106 + .moveTo((-eyebrowWidth / 2) * this.size, 0) 107 + .quadraticCurveTo(0, eyebrowCurve * this.size, (eyebrowWidth / 2) * this.size, 0) 108 + .stroke({ 109 + color: opts.eyebrow?.color ?? 0, 110 + width: opts.eyebrow?.width ?? this.size * 0.2 111 + }); 112 + eyebrow.position.set(opts.eyebrow?.x ?? 0, (opts.eyebrow?.y ?? 0.9) * this.size); 123 113 124 - this.container.addChild(eyebrow); 125 - } 114 + // this.container.addChild(eyebrow); 115 + } 126 116 127 117 lookInDirection(direction: number) { 128 118 const angle = degreesToRadians(direction);
+74 -21
src/lib/face/face.ts
··· 7 7 8 8 let mouth: Mouth; 9 9 10 + const mouthSettings = { 11 + x: 0, 12 + y: -130, 13 + stroke: 20, 14 + color: 0, 15 + width: 100, 16 + curve: -40 17 + }; 18 + 10 19 export async function createFaceTexture({ 11 20 width = 512, 12 21 height = 512 ··· 15 24 height?: number; 16 25 }): Promise<{ app: Application<Renderer> }> { 17 26 const xScale = 0.4; 18 - const yScale = 1.7; 27 + const yScale = 1.5; 19 28 const app = new Application(); 20 29 21 30 await app.init({ ··· 45 54 container.addChild(rightEye.container); 46 55 container.addChild(leftEye.container); 47 56 48 - mouth = new Mouth({ 49 - x: 0, 50 - y: -130, 51 - size: 50 52 - }); 57 + mouth = new Mouth(mouthSettings); 53 58 container.addChild(mouth.container); 54 59 55 60 return { ··· 65 70 height?: number; 66 71 }): Promise<{ app: Application<Renderer> }> { 67 72 const xScale = 0.4; 68 - const yScale = 1.45; 73 + const yScale = 1.5; 69 74 const app = new Application(); 70 75 71 76 await app.init({ 72 77 width, 73 78 height, 74 - backgroundAlpha: 0.0 79 + backgroundAlpha: 0.0, 80 + antialias: true 75 81 }); 76 82 77 83 const container = new Container(); ··· 83 89 const background = new Graphics(); 84 90 background.rect(0, 0, width * 5, height * 5); 85 91 background.pivot.set(width * 2.5, height * 2.5); 86 - background.fill({ color: '#000000' }); 92 + background.fill({ color: 'black' }); 87 93 container.addChild(background); 88 94 95 + // draw 50 random colored circles in random positions 96 + // for (let i = 0; i < 500; i++) { 97 + // const circle = new Graphics(); 98 + // circle.circle(0, 0, 1 + Math.random() * 20 + 10); 99 + // circle.x = (Math.random() * width * 0.8 - width * 0.4) / xScale; 100 + // circle.y = (Math.random() * height * 0.4 - height * 0.2) / yScale; 101 + 102 + // // if (Math.hypot(circle.x, circle.y) < 150) continue; 103 + 104 + // circle.fill({ 105 + // color: { 106 + // r: Math.random() * 256, 107 + // g: Math.random() * 256, 108 + // b: Math.random() * 256, 109 + // a: Math.random() 110 + // } 111 + // }); 112 + // container.addChild(circle); 113 + // } 114 + 89 115 // add circle mask 90 116 const circleMask = new Graphics(); 91 117 circleMask.circle(0, 0, 100); ··· 94 120 circleMask.fill({ color: '#ffffff' }); 95 121 container.addChild(circleMask); 96 122 97 - // for (let i = 0; i < 21; i++) { 98 - // // add circle 99 - // const circle = new Graphics(); 100 - // circle.circle(0, 0, 3 * i + 40); 101 - // // more and more green 102 - // circle.stroke({ color: { r: 0, g: 0, b: 0, a: i / 20 }, width: 3 }); 103 - // container.addChild(circle); 104 - // } 123 + for (let i = 0; i < 21; i++) { 124 + // add circle 125 + const circle = new Graphics(); 126 + circle.circle(0, 0, 5 * i - 2.5); 127 + circle.stroke({ color: { r: 0, g: 0, b: 0, a: Math.pow(i / 21, 10) + 0.7 }, width: 5 }); 128 + container.addChild(circle); 129 + } 105 130 106 - const circle = new Graphics(); 107 - circle.circle(0, 0, 100); 108 - circle.fill({ color: { r: 0, g: 0, b: 0, a: 0.65 } }); 109 - container.addChild(circle); 131 + // const circle = new Graphics(); 132 + // circle.circle(0, 0, 100); 133 + // circle.fill({ color: { r: 256, g: 256, b: 256, a: 0.65 } }); 134 + // container.addChild(circle); 110 135 111 136 const eyeMaskLeft = new Graphics(); 112 137 eyeMaskLeft.circle(0, 0, 25); ··· 123 148 eyeMaskRight.blendMode = 'erase'; 124 149 eyeMaskRight.fill({ color: '#ffffff' }); 125 150 container.addChild(eyeMaskRight); 151 + 152 + const eyebrowLeft = new Mouth({ 153 + x: 75 / 2, 154 + y: 40, 155 + width: 50, 156 + curve: 10, 157 + stroke: 10 158 + }); 159 + eyebrowLeft.container.alpha = 0.5; 160 + container.addChild(eyebrowLeft.container); 161 + 162 + const eyebrowRight = new Mouth({ 163 + x: -75 / 2, 164 + y: 40, 165 + width: 50, 166 + curve: 10, 167 + stroke: 10 168 + }); 169 + eyebrowRight.container.alpha = 0.5; 170 + container.addChild(eyebrowRight.container); 171 + 172 + const mouth3 = new Mouth({ 173 + ...mouthSettings, 174 + scale: 0.5 175 + }); 176 + // mouth3.container.alpha = 0.5; 177 + mouth3.container.blendMode = 'erase'; 178 + container.addChild(mouth3.container); 126 179 127 180 return { 128 181 app
+32 -68
src/lib/face/mouth.ts
··· 1 - import * as PIXI from "pixi.js"; 2 - import { AnimatedValue, degreesToRadians } from "./math-helper"; 1 + import * as PIXI from 'pixi.js'; 3 2 4 3 export type EyeOptions = { 5 - container: PIXI.Container; 4 + container: PIXI.Container; 6 5 7 - x: number; 8 - y: number; 6 + scale: number; 9 7 10 - eyeColor: PIXI.ColorSource; 11 - pupilColor: PIXI.ColorSource; 12 - highlightColor: PIXI.ColorSource; 8 + stroke: number; 13 9 14 - size: number; 15 - 16 - eyebrow: Partial<{ 17 - length: number; 18 - 19 - width: number; 10 + width: number; 20 11 21 - y: number; 22 - x: number; 23 - 24 - curve: number; 12 + curve: number; 25 13 26 - color: PIXI.ColorSource; 27 - }>; 14 + color: PIXI.ColorSource; 28 15 29 - blinkStart: number; 30 - blinkTimer: number; 16 + x: number; 17 + y: number; 31 18 }; 32 19 33 20 export default class Mouth { 34 - eyeContainer: PIXI.Container; 21 + container: PIXI.Container; 35 22 36 - container: PIXI.Container; 37 - 38 - base?: PIXI.Graphics; 39 - 40 - _x: AnimatedValue; 41 - _y: AnimatedValue; 42 - 43 - dx: number = 0; 44 - dy: number = 0; 45 - 46 - size: number = 1; 47 - 48 - targetX: number = 0; 49 - targetY: number = 0; 50 - 51 - constructor(opts: Partial<EyeOptions>) { 52 - console.log("Eye", opts); 53 - this.container = new PIXI.Container(); 23 + scale: number = 1; 54 24 55 - this.eyeContainer = new PIXI.Container(); 25 + x: number = 0; 26 + y: number = 0; 56 27 57 - this.container.position.set(opts.x ?? 0, opts.y ?? 0); 28 + constructor(opts: Partial<EyeOptions>) { 29 + this.container = new PIXI.Container(); 58 30 59 - this.size = opts.size ?? 1; 31 + this.scale = opts?.scale ?? 1; 60 32 61 - this._x = new AnimatedValue(); 62 - this._y = new AnimatedValue(); 33 + this.x = opts?.x ?? 0; 34 + this.y = opts?.y ?? 0; 63 35 64 - this.container.addChild(this.eyeContainer); 36 + if (opts.container) opts.container.addChild(this.container); 65 37 66 - if (opts.container) opts.container.addChild(this.container); 38 + const width = opts?.width ?? 50; 39 + const curve = opts?.curve ?? 10; 67 40 68 - let eyebrowWidth = opts?.eyebrow?.width ?? 2; 69 - let eyebrowCurve = opts?.eyebrow?.curve ?? -0.3; 41 + const eyebrow = new PIXI.Graphics() 42 + .moveTo((-width / 2) * this.scale, 0) 43 + .quadraticCurveTo(0, curve * this.scale, (width / 2) * this.scale, 0) 44 + .stroke({ 45 + color: opts?.color ?? 0x101010, 46 + width: (opts?.stroke ?? 5) * this.scale, 47 + cap: 'round' 48 + }); 70 49 71 - const eyebrow = new PIXI.Graphics() 72 - .moveTo((-eyebrowWidth / 2) * this.size, 0) 73 - .quadraticCurveTo( 74 - 0, 75 - eyebrowCurve * this.size, 76 - (eyebrowWidth / 2) * this.size, 77 - 0, 78 - ) 79 - .stroke({ 80 - color: opts.eyebrow?.color ?? 0, 81 - width: opts.eyebrow?.width ?? this.size * 0.2, 82 - }); 83 - eyebrow.position.set( 84 - opts.eyebrow?.x ?? 0, 85 - (opts.eyebrow?.y ?? 0.9) * this.size, 86 - ); 50 + eyebrow.position.set(this.x * this.scale, this.y * this.scale); 87 51 88 - this.container.addChild(eyebrow); 89 - } 52 + this.container.addChild(eyebrow); 53 + } 90 54 }
-1
src/lib/index.ts
··· 1 - // place files you want to import through the `$lib` alias in this folder.
-175
src/lib/main.ts
··· 1 - import * as THREE from 'three'; 2 - // @ts-ignore 3 - import { WiggleBone } from 'wiggle'; 4 - import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; 5 - import { createFaceTexture, updateFace } from './face/face'; 6 - 7 - const setup = async () => { 8 - // Create the scene 9 - const scene = new THREE.Scene(); 10 - 11 - const mouse = new THREE.Vector2(); 12 - 13 - // // Create the camera 14 - // const camera = new THREE.PerspectiveCamera( 15 - // 75, // Field of view 16 - // window.innerWidth / window.innerHeight, // Aspect ratio 17 - // ); 18 - 19 - // // Position the camera 20 - // camera.position.z = 7; 21 - // camera.position.y = 7; 22 - // camera.position.x = -7; 23 - 24 - const s = 7; 25 - const width = window.innerWidth * 0.015; 26 - const height = window.innerHeight * 0.015; 27 - const camera = new THREE.OrthographicCamera( 28 - width / -2, 29 - width / 2, 30 - height / 2, 31 - height / -2, 32 - 0.1, 33 - 100 34 - ); 35 - camera.position.z = 7; 36 - camera.position.y = 7; 37 - camera.position.x = -7; 38 - 39 - // look at the origin 40 - camera.lookAt(0, 0, 0); 41 - 42 - // Set up the renderer 43 - const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); 44 - renderer.setSize(window.innerWidth, window.innerHeight); 45 - document.body.appendChild(renderer.domElement); 46 - 47 - // set styles of the canvas 48 - renderer.domElement.style.position = 'absolute'; 49 - renderer.domElement.style.top = '0'; 50 - renderer.domElement.style.left = '0'; 51 - renderer.domElement.style.width = '100%'; 52 - renderer.domElement.style.height = '100%'; 53 - 54 - renderer.outputColorSpace = THREE.SRGBColorSpace; // optional with post-processing 55 - renderer.toneMapping = THREE.ACESFilmicToneMapping; 56 - 57 - const { app } = await createFaceTexture({ 58 - width: 512, 59 - height: 512 60 - }); 61 - 62 - const texture = new THREE.CanvasTexture(app.canvas); 63 - 64 - const loader = new GLTFLoader(); 65 - 66 - let rootBone: THREE.Bone; 67 - const wiggleBones: WiggleBone[] = []; 68 - 69 - let creature: THREE.Group; 70 - 71 - loader.load('/tiny-creature/model.glb', ({ scene: obj }) => { 72 - const mesh = obj.getObjectByName('Icosphere'); 73 - if (!mesh) return; 74 - 75 - creature = obj; 76 - 77 - if (mesh instanceof THREE.Mesh) { 78 - mesh.material = new THREE.MeshStandardMaterial({ 79 - map: texture 80 - }); 81 - } 82 - 83 - // obj.rotation.y = -Math.PI / 2; 84 - obj.lookAt(camera.position); 85 - 86 - if (mesh instanceof THREE.SkinnedMesh) { 87 - mesh.skeleton.bones.forEach((bone) => { 88 - if (!(bone.parent instanceof THREE.Bone) || !bone.parent?.isBone) { 89 - rootBone = bone; 90 - 91 - // rootBone.rotation.x = Math.PI / 2; 92 - } else { 93 - const wiggleBone = new WiggleBone(bone, { 94 - velocity: 0.4 95 - }); 96 - wiggleBones.push(wiggleBone); 97 - } 98 - }); 99 - } 100 - 101 - scene.add(obj); 102 - }); 103 - 104 - loader.load('/tiny-creature/room.glb', ({ scene: obj }) => { 105 - scene.add(obj); 106 - console.log(obj); 107 - // find mesh named Cube 108 - const mesh = obj.getObjectByName('Cube'); 109 - if (mesh instanceof THREE.Mesh) { 110 - mesh.material = new THREE.MeshStandardMaterial({ 111 - color: 0xb1b1b1, 112 - side: THREE.DoubleSide 113 - }); 114 - } 115 - 116 - // find Light object 117 - const light = obj.getObjectByName('Light'); 118 - // add light there 119 - if (light) { 120 - const pointLight = new THREE.PointLight(0xffffff, 0.7); 121 - pointLight.position.copy(light.position); 122 - scene.add(pointLight); 123 - } 124 - 125 - // find Light object 126 - const light2 = obj.getObjectByName('Light2'); 127 - if (light2) { 128 - const pointLight = new THREE.PointLight(0xffffff, 0.4); 129 - pointLight.position.copy(light2.position); 130 - scene.add(pointLight); 131 - } 132 - }); 133 - 134 - const light = new THREE.DirectionalLight(0xffffff, 0.2); 135 - light.position.set(10, 10, 10); 136 - scene.add(light); 137 - 138 - const light2 = new THREE.DirectionalLight(0xffffff, 0.1); 139 - light2.position.set(-10, 10, -10); 140 - scene.add(light2); 141 - 142 - const ambientLight = new THREE.AmbientLight(0xffffff, 0.2); 143 - scene.add(ambientLight); 144 - 145 - let lastTime = performance.now(); 146 - const animate = () => { 147 - requestAnimationFrame(animate); 148 - texture.needsUpdate = true; 149 - 150 - const deltaTime = (performance.now() - lastTime) / 1000; 151 - lastTime = performance.now(); 152 - 153 - updateFace(deltaTime, mouse.x, mouse.y); 154 - 155 - if (rootBone) { 156 - creature.position.z = -mouse.x * 2; 157 - creature.position.x = mouse.y * 2; 158 - creature.position.y = 0.5; 159 - 160 - // look at camera 161 - creature.lookAt(new THREE.Vector3(camera.position.x, 0, camera.position.z)); 162 - rootBone.rotation.y = -Math.PI / 2; 163 - } 164 - 165 - wiggleBones.forEach((wiggleBone) => { 166 - wiggleBone.update(); 167 - }); 168 - 169 - renderer.render(scene, camera); 170 - }; 171 - 172 - animate(); 173 - }; 174 - 175 - setup();
+1 -1
src/routes/+page.svelte
··· 7 7 8 8 </script> 9 9 10 - <div class="h-screen w-screen"> 10 + <div class="h-screen w-screen bg-gradient-to-t from-stone-950 to-stone-900/50"> 11 11 <Canvas createRenderer={(canvas) => { 12 12 return new WebGPURenderer({ 13 13 canvas,
static/model.glb

This is a binary file and will not be displayed.

static/room.glb

This is a binary file and will not be displayed.

static/room.jpg

This is a binary file and will not be displayed.