tone down duck ruffles, add water anim improvements
- SHA
b334448425f1a630b536b9303b8a0a67faee655d- Parents
-
ea0aa57 - Tree
866720f
b334448
b334448425f1a630b536b9303b8a0a67faee655dea0aa57
866720f| Status | File | + | - |
|---|---|---|---|
| M |
src/renderers/three/duck.js
|
49 | 32 |
| M |
src/renderers/three/pond.js
|
82 | 12 |
src/renderers/three/duck.jsmodified@@ -59,18 +59,15 @@ export function createDoug(scene, gradientMap) { | ||
| 59 | 59 | chest.position.set(0.25, 0.32, 0) |
| 60 | 60 | group.add(chest) |
| 61 | 61 | |
| 62 | - // Scraggly feather tufts on body | |
| 62 | + // Subtle feather tufts on body (toned down) | |
| 63 | 63 | const tuftMaterial = bodyMaterial |
| 64 | 64 | const featherPositions = [ |
| 65 | - { x: -0.4, y: 0.45, z: 0.15, rx: 0.3, rz: 0.5 }, | |
| 66 | - { x: -0.45, y: 0.4, z: -0.12, rx: -0.2, rz: 0.4 }, | |
| 67 | - { x: -0.3, y: 0.48, z: 0, rx: 0, rz: 0.3 }, | |
| 68 | - { x: 0, y: 0.5, z: 0.25, rx: 0.4, rz: -0.2 }, | |
| 69 | - { x: 0, y: 0.5, z: -0.25, rx: -0.4, rz: -0.2 }, | |
| 65 | + { x: -0.38, y: 0.44, z: 0.1, rx: 0.2, rz: 0.3 }, | |
| 66 | + { x: -0.38, y: 0.44, z: -0.1, rx: -0.2, rz: 0.3 }, | |
| 70 | 67 | ] |
| 71 | 68 | |
| 72 | 69 | for (const f of featherPositions) { |
| 73 | - const featherGeom = new THREE.ConeGeometry(0.06, 0.18, 4) | |
| 70 | + const featherGeom = new THREE.ConeGeometry(0.04, 0.12, 4) | |
| 74 | 71 | const feather = new THREE.Mesh(featherGeom, tuftMaterial) |
| 75 | 72 | feather.position.set(f.x, f.y, f.z) |
| 76 | 73 | feather.rotation.x = f.rx |
@@ -78,18 +75,16 @@ export function createDoug(scene, gradientMap) { | ||
| 78 | 75 | group.add(feather) |
| 79 | 76 | } |
| 80 | 77 | |
| 81 | - // Tail feathers - more prominent and scraggly | |
| 78 | + // Tail feathers - subtle curl up | |
| 82 | 79 | const tailGroup = new THREE.Group() |
| 83 | 80 | const tailFeathers = [ |
| 84 | - { x: -0.58, y: 0.38, z: 0, rx: 1.8, rz: 0, scale: 1.2 }, | |
| 85 | - { x: -0.62, y: 0.42, z: 0.1, rx: 1.6, rz: 0.3, scale: 1.0 }, | |
| 86 | - { x: -0.62, y: 0.42, z: -0.1, rx: 1.6, rz: -0.3, scale: 1.0 }, | |
| 87 | - { x: -0.55, y: 0.48, z: 0.05, rx: 1.4, rz: 0.15, scale: 0.8 }, | |
| 88 | - { x: -0.55, y: 0.48, z: -0.05, rx: 1.4, rz: -0.15, scale: 0.8 }, | |
| 81 | + { x: -0.55, y: 0.36, z: 0, rx: 1.6, rz: 0, scale: 1.0 }, | |
| 82 | + { x: -0.52, y: 0.40, z: 0.08, rx: 1.4, rz: 0.2, scale: 0.7 }, | |
| 83 | + { x: -0.52, y: 0.40, z: -0.08, rx: 1.4, rz: -0.2, scale: 0.7 }, | |
| 89 | 84 | ] |
| 90 | 85 | |
| 91 | 86 | for (const t of tailFeathers) { |
| 92 | - const tailGeom = new THREE.ConeGeometry(0.05, 0.25, 4) | |
| 87 | + const tailGeom = new THREE.ConeGeometry(0.04, 0.18, 4) | |
| 93 | 88 | const tail = new THREE.Mesh(tailGeom, bodyMaterial) |
| 94 | 89 | tail.position.set(t.x, t.y, t.z) |
| 95 | 90 | tail.rotation.x = t.rx |
@@ -148,27 +143,20 @@ export function createDoug(scene, gradientMap) { | ||
| 148 | 143 | rightCheek.scale.set(0.8, 0.7, 0.6) |
| 149 | 144 | group.add(rightCheek) |
| 150 | 145 | |
| 151 | - // Head tuft - messier, multiple feathers | |
| 152 | - const tuftGeom = new THREE.ConeGeometry(0.04, 0.14, 4) | |
| 146 | + // Head tuft - simple pair of feathers | |
| 147 | + const tuftGeom = new THREE.ConeGeometry(0.035, 0.12, 4) | |
| 153 | 148 | const tuft1 = new THREE.Mesh(tuftGeom, bodyMaterial) |
| 154 | - tuft1.position.set(0.35, 0.95, 0) | |
| 155 | - tuft1.rotation.z = -0.4 | |
| 149 | + tuft1.position.set(0.34, 0.93, 0.03) | |
| 150 | + tuft1.rotation.z = -0.3 | |
| 151 | + tuft1.rotation.x = 0.2 | |
| 156 | 152 | group.add(tuft1) |
| 157 | 153 | |
| 158 | 154 | const tuft2 = new THREE.Mesh(tuftGeom, bodyMaterial) |
| 159 | - tuft2.position.set(0.32, 0.92, 0.06) | |
| 160 | - tuft2.rotation.z = -0.2 | |
| 161 | - tuft2.rotation.x = 0.3 | |
| 162 | - tuft2.scale.setScalar(0.8) | |
| 155 | + tuft2.position.set(0.34, 0.93, -0.03) | |
| 156 | + tuft2.rotation.z = -0.3 | |
| 157 | + tuft2.rotation.x = -0.2 | |
| 163 | 158 | group.add(tuft2) |
| 164 | 159 | |
| 165 | - const tuft3 = new THREE.Mesh(tuftGeom, bodyMaterial) | |
| 166 | - tuft3.position.set(0.32, 0.92, -0.06) | |
| 167 | - tuft3.rotation.z = -0.2 | |
| 168 | - tuft3.rotation.x = -0.3 | |
| 169 | - tuft3.scale.setScalar(0.7) | |
| 170 | - group.add(tuft3) | |
| 171 | - | |
| 172 | 160 | // Beak - flatter, more duck-like |
| 173 | 161 | const beakGeom = new THREE.ConeGeometry(0.09, 0.32, 6) |
| 174 | 162 | beakGeom.scale(1, 1, 0.6) // Flatten it |
@@ -228,7 +216,9 @@ export function createDoug(scene, gradientMap) { | ||
| 228 | 216 | idleTimer: 0, |
| 229 | 217 | nextIdleMove: 3 + Math.random() * 4, |
| 230 | 218 | wobble: 0, |
| 231 | - headBob: 0 | |
| 219 | + headBob: 0, | |
| 220 | + rippleTimer: 0, | |
| 221 | + isMoving: false | |
| 232 | 222 | } |
| 233 | 223 | |
| 234 | 224 | // Movement speeds |
@@ -323,18 +313,35 @@ export function createDoug(scene, gradientMap) { | ||
| 323 | 313 | |
| 324 | 314 | // Wobble animation while moving |
| 325 | 315 | state.wobble += delta * 8 |
| 316 | + state.isMoving = true | |
| 317 | + | |
| 318 | + // Create swimming ripples | |
| 319 | + state.rippleTimer += delta | |
| 320 | + const rippleInterval = state.mode === 'swimming' ? 0.25 : 0.5 | |
| 321 | + if (state.rippleTimer >= rippleInterval) { | |
| 322 | + state.rippleTimer = 0 | |
| 323 | + // Ripple slightly behind the duck | |
| 324 | + const rippleX = state.position.x - Math.sin(state.rotation) * 0.3 | |
| 325 | + const rippleZ = state.position.z - Math.cos(state.rotation) * 0.3 | |
| 326 | + pond.addRipple(rippleX, rippleZ) | |
| 327 | + } | |
| 328 | + } else { | |
| 329 | + state.isMoving = false | |
| 326 | 330 | } |
| 327 | 331 | } else { |
| 332 | + state.isMoving = false | |
| 328 | 333 | // Arrived |
| 329 | 334 | if (state.mode === 'swimming') { |
| 330 | 335 | state.mode = 'idle' |
| 331 | - // Eat nearby bread | |
| 336 | + // Eat nearby bread - create splash ripple! | |
| 332 | 337 | for (const bread of breadBits) { |
| 333 | 338 | if (!bread.eaten) { |
| 334 | 339 | const bx = bread.position.x - state.position.x |
| 335 | 340 | const bz = bread.position.z - state.position.z |
| 336 | 341 | if (Math.sqrt(bx * bx + bz * bz) < 0.4) { |
| 337 | 342 | bread.eaten = true |
| 343 | + // Eating splash ripple | |
| 344 | + pond.addRipple(state.position.x, state.position.z) | |
| 338 | 345 | } |
| 339 | 346 | } |
| 340 | 347 | } |
@@ -356,7 +363,17 @@ export function createDoug(scene, gradientMap) { | ||
| 356 | 363 | group.position.z = state.position.z |
| 357 | 364 | |
| 358 | 365 | // Bobbing on water |
| 359 | - group.position.y = Math.sin(elapsed * 2) * 0.03 | |
| 366 | + const bobAmount = Math.sin(elapsed * 2) | |
| 367 | + group.position.y = bobAmount * 0.03 | |
| 368 | + | |
| 369 | + // Occasional idle bob ripples (when bobbing down) | |
| 370 | + if (!state.isMoving && bobAmount < -0.9 && state.rippleTimer > 1.5) { | |
| 371 | + state.rippleTimer = 0 | |
| 372 | + pond.addRipple(state.position.x, state.position.z) | |
| 373 | + } | |
| 374 | + if (!state.isMoving) { | |
| 375 | + state.rippleTimer += delta | |
| 376 | + } | |
| 360 | 377 | |
| 361 | 378 | // Rotation (face direction of movement) |
| 362 | 379 | // Duck model faces +X locally, so offset by -PI/2 to align with movement |
src/renderers/three/pond.jsmodified@@ -49,38 +49,78 @@ export function createPond(scene, gradientMap) { | ||
| 49 | 49 | sand.position.y = -0.02 |
| 50 | 50 | group.add(sand) |
| 51 | 51 | |
| 52 | - // Water surface | |
| 53 | - const waterGeom = new THREE.CircleGeometry(radius, 32) | |
| 52 | + // Water surface - higher resolution for wave animation | |
| 53 | + const waterGeom = new THREE.CircleGeometry(radius, 48, 8) | |
| 54 | 54 | waterGeom.rotateX(-Math.PI / 2) |
| 55 | 55 | const water = new THREE.Mesh(waterGeom, waterMaterial) |
| 56 | 56 | water.position.y = 0 |
| 57 | 57 | group.add(water) |
| 58 | 58 | |
| 59 | - // Water depth visual (darker center) | |
| 60 | - const deepGeom = new THREE.CircleGeometry(radius * 0.6, 24) | |
| 59 | + // Store original water vertex positions for wave animation | |
| 60 | + const waterPositions = waterGeom.attributes.position.array.slice() | |
| 61 | + | |
| 62 | + // Water depth visual (darker center with gradient) | |
| 63 | + const deepGeom = new THREE.CircleGeometry(radius * 0.7, 32) | |
| 61 | 64 | deepGeom.rotateX(-Math.PI / 2) |
| 62 | 65 | const deepMaterial = new THREE.MeshToonMaterial({ |
| 63 | 66 | color: waterDeep, |
| 64 | 67 | gradientMap: gradientMap, |
| 65 | 68 | transparent: true, |
| 66 | - opacity: 0.5 | |
| 69 | + opacity: 0.6 | |
| 67 | 70 | }) |
| 68 | 71 | const deep = new THREE.Mesh(deepGeom, deepMaterial) |
| 69 | - deep.position.y = -0.01 | |
| 72 | + deep.position.y = -0.02 | |
| 70 | 73 | group.add(deep) |
| 71 | 74 | |
| 72 | - // Water highlight (light reflection) | |
| 73 | - const highlightGeom = new THREE.CircleGeometry(radius * 0.3, 16) | |
| 75 | + // Secondary highlight shimmer | |
| 76 | + const shimmerGeom = new THREE.CircleGeometry(radius * 0.5, 24) | |
| 77 | + shimmerGeom.rotateX(-Math.PI / 2) | |
| 78 | + const shimmerMaterial = new THREE.MeshBasicMaterial({ | |
| 79 | + color: 0x6bc4d8, | |
| 80 | + transparent: true, | |
| 81 | + opacity: 0.25 | |
| 82 | + }) | |
| 83 | + const shimmer = new THREE.Mesh(shimmerGeom, shimmerMaterial) | |
| 84 | + shimmer.position.set(radius * 0.15, 0.01, radius * 0.15) | |
| 85 | + group.add(shimmer) | |
| 86 | + | |
| 87 | + // Main highlight (sun reflection) | |
| 88 | + const highlightGeom = new THREE.CircleGeometry(radius * 0.25, 16) | |
| 74 | 89 | highlightGeom.rotateX(-Math.PI / 2) |
| 75 | 90 | const highlightMaterial = new THREE.MeshBasicMaterial({ |
| 76 | - color: 0x88d4e8, | |
| 91 | + color: 0xa8e8f8, | |
| 77 | 92 | transparent: true, |
| 78 | - opacity: 0.4 | |
| 93 | + opacity: 0.5 | |
| 79 | 94 | }) |
| 80 | 95 | const highlight = new THREE.Mesh(highlightGeom, highlightMaterial) |
| 81 | 96 | highlight.position.set(-radius * 0.35, 0.02, -radius * 0.35) |
| 82 | 97 | group.add(highlight) |
| 83 | 98 | |
| 99 | + // Small sparkle highlights | |
| 100 | + const sparkles = [] | |
| 101 | + const sparkleMaterial = new THREE.MeshBasicMaterial({ | |
| 102 | + color: 0xffffff, | |
| 103 | + transparent: true, | |
| 104 | + opacity: 0.7 | |
| 105 | + }) | |
| 106 | + for (let i = 0; i < 6; i++) { | |
| 107 | + const sparkleGeom = new THREE.CircleGeometry(0.08 + Math.random() * 0.06, 6) | |
| 108 | + sparkleGeom.rotateX(-Math.PI / 2) | |
| 109 | + const sparkle = new THREE.Mesh(sparkleGeom, sparkleMaterial.clone()) | |
| 110 | + const angle = Math.random() * Math.PI * 2 | |
| 111 | + const dist = Math.random() * radius * 0.8 | |
| 112 | + sparkle.position.set( | |
| 113 | + Math.cos(angle) * dist, | |
| 114 | + 0.03, | |
| 115 | + Math.sin(angle) * dist | |
| 116 | + ) | |
| 117 | + sparkle.userData.baseX = sparkle.position.x | |
| 118 | + sparkle.userData.baseZ = sparkle.position.z | |
| 119 | + sparkle.userData.phase = Math.random() * Math.PI * 2 | |
| 120 | + group.add(sparkle) | |
| 121 | + sparkles.push(sparkle) | |
| 122 | + } | |
| 123 | + | |
| 84 | 124 | // Grass tufts around the pond |
| 85 | 125 | const grassTuftGeom = new THREE.ConeGeometry(0.15, 0.3, 4) |
| 86 | 126 | const grassMaterial = new THREE.MeshToonMaterial({ |
@@ -396,9 +436,39 @@ export function createPond(scene, gradientMap) { | ||
| 396 | 436 | let smokeSpawnTimer = 0 |
| 397 | 437 | |
| 398 | 438 | function update(delta, elapsed) { |
| 439 | + // Animate water surface waves | |
| 440 | + const positions = waterGeom.attributes.position.array | |
| 441 | + for (let i = 0; i < positions.length; i += 3) { | |
| 442 | + const x = waterPositions[i] | |
| 443 | + const z = waterPositions[i + 2] | |
| 444 | + const dist = Math.sqrt(x * x + z * z) | |
| 445 | + | |
| 446 | + // Gentle concentric waves from center | |
| 447 | + const wave1 = Math.sin(dist * 1.5 - elapsed * 2) * 0.03 | |
| 448 | + // Cross-wave pattern | |
| 449 | + const wave2 = Math.sin(x * 0.8 + elapsed * 1.5) * Math.cos(z * 0.8 + elapsed * 1.2) * 0.02 | |
| 450 | + | |
| 451 | + positions[i + 1] = waterPositions[i + 1] + wave1 + wave2 | |
| 452 | + } | |
| 453 | + waterGeom.attributes.position.needsUpdate = true | |
| 454 | + | |
| 399 | 455 | // Animate water highlight |
| 400 | - highlight.position.x = -radius * 0.35 + Math.sin(elapsed * 0.5) * 0.2 | |
| 401 | - highlight.position.z = -radius * 0.35 + Math.cos(elapsed * 0.5) * 0.2 | |
| 456 | + highlight.position.x = -radius * 0.35 + Math.sin(elapsed * 0.5) * 0.3 | |
| 457 | + highlight.position.z = -radius * 0.35 + Math.cos(elapsed * 0.5) * 0.3 | |
| 458 | + highlight.material.opacity = 0.4 + Math.sin(elapsed * 2) * 0.1 | |
| 459 | + | |
| 460 | + // Animate shimmer | |
| 461 | + shimmer.position.x = radius * 0.15 + Math.cos(elapsed * 0.4) * 0.2 | |
| 462 | + shimmer.position.z = radius * 0.15 + Math.sin(elapsed * 0.3) * 0.2 | |
| 463 | + shimmer.material.opacity = 0.2 + Math.sin(elapsed * 1.5 + 1) * 0.1 | |
| 464 | + | |
| 465 | + // Animate sparkles - twinkle effect | |
| 466 | + for (const sparkle of sparkles) { | |
| 467 | + const twinkle = Math.sin(elapsed * 4 + sparkle.userData.phase) | |
| 468 | + sparkle.material.opacity = twinkle > 0.3 ? 0.8 : 0 | |
| 469 | + sparkle.position.x = sparkle.userData.baseX + Math.sin(elapsed * 0.5 + sparkle.userData.phase) * 0.1 | |
| 470 | + sparkle.position.z = sparkle.userData.baseZ + Math.cos(elapsed * 0.5 + sparkle.userData.phase) * 0.1 | |
| 471 | + } | |
| 402 | 472 | |
| 403 | 473 | // Update ripples |
| 404 | 474 | for (let i = ripples.length - 1; i >= 0; i--) { |