Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/aura/AuraCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function AuraCanvas({
aria-label={`${name} 3D 오라 장면`}
data-seed={plan.seed}
data-variation={plan.variation}
data-family={plan.flower.family}
/>
)
}
102 changes: 46 additions & 56 deletions src/aura/renderer/AuraRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as THREE from 'three'
import { createSeededRandom, hashText } from '../scenePlan'
import type { AuraPalette, AuraScenePlan, RenderQuality } from '../types'
import { createAuraForm, type AuraColors, type AuraFormPart } from './auraForms'

type Palette = { background: number; primary: number; secondary: number; core: number }
const PALETTES: Record<AuraPalette, Palette> = {
const PALETTES: Record<AuraPalette, AuraColors> = {
dawn: { background: 0x100d0b, primary: 0xffb47a, secondary: 0xf9dec8, core: 0xfff3d0 },
ocean: { background: 0x071012, primary: 0x5dbec4, secondary: 0x8dafa7, core: 0xd9fff4 },
ember: { background: 0x110a08, primary: 0xe56d3b, secondary: 0xf0aa55, core: 0xffe0a0 },
Expand All @@ -23,7 +23,7 @@ export class AuraRenderer {
private readonly camera = new THREE.PerspectiveCamera(38, 1, 0.1, 100)
private readonly renderer: THREE.WebGLRenderer
private readonly flower = new THREE.Group()
private readonly petals: Array<{ mesh: THREE.Mesh; baseScale: THREE.Vector3 }> = []
private readonly formParts: AuraFormPart[]
private readonly particles: THREE.Points
private readonly resizeObserver: ResizeObserver
private animationFrame: number | null = null
Expand Down Expand Up @@ -55,7 +55,7 @@ export class AuraRenderer {
this.camera.position.set(0, 0.25, 6.3)
this.scene.add(this.flower)
this.addLighting(palette)
this.addFlower(palette, settings.segments)
this.formParts = createAuraForm(this.flower, plan, palette, settings.segments)
this.particles = this.addParticles(palette, settings.particles)
this.addAtmosphere(palette)
this.resizeObserver = new ResizeObserver(() => this.resize())
Expand All @@ -67,7 +67,7 @@ export class AuraRenderer {
this.start()
}

private addLighting(palette: Palette): void {
private addLighting(palette: AuraColors): void {
this.scene.add(new THREE.AmbientLight(palette.secondary, 1.4))
const key = new THREE.DirectionalLight(palette.core, 4.2)
key.position.set(2.5, 3.5, 4)
Expand All @@ -77,50 +77,7 @@ export class AuraRenderer {
this.scene.add(rim)
}

private createPetalGeometry(segments: number): THREE.BufferGeometry {
if (this.plan.flower.family === 'crystal') return new THREE.ConeGeometry(0.42, 1.8, 5, 1)
if (this.plan.flower.family === 'ribbon') return new THREE.SphereGeometry(0.6, segments, 6)
return new THREE.SphereGeometry(0.64, segments, Math.max(8, Math.floor(segments / 2)))
}

private addFlower(palette: Palette, segments: number): void {
const random = createSeededRandom(hashText(`${this.plan.seed}:${this.plan.variation}:flower`))
const geometry = this.createPetalGeometry(segments)
const material = new THREE.MeshPhysicalMaterial({
color: palette.primary,
emissive: palette.secondary,
emissiveIntensity: 0.14 + this.plan.atmosphere.bloomStrength * 0.4,
roughness: 0.35 + this.plan.flower.sharpness * 0.22,
metalness: this.plan.flower.family === 'crystal' ? 0.62 : 0.12,
transparent: true,
opacity: 0.82,
side: THREE.DoubleSide,
})
for (let index = 0; index < this.plan.flower.petalCount; index += 1) {
const angle = (index / this.plan.flower.petalCount) * Math.PI * 2
const mesh = new THREE.Mesh(geometry, material)
const length = this.plan.flower.radius * (0.78 + random() * 0.2)
const width = 0.34 + (1 - this.plan.flower.sharpness) * 0.25
const thickness = this.plan.flower.family === 'ribbon' ? 0.08 : 0.2
mesh.scale.set(width, length, thickness)
mesh.position.set(Math.cos(angle) * 0.62, Math.sin(angle) * 0.62, (random() - 0.5) * 0.34)
mesh.rotation.set(0.3 + this.plan.flower.openness * 0.55, 0, angle - Math.PI / 2)
this.flower.add(mesh)
this.petals.push({ mesh, baseScale: mesh.scale.clone() })
}
const core = new THREE.Mesh(
new THREE.IcosahedronGeometry(0.55, 2),
new THREE.MeshPhysicalMaterial({
color: palette.core,
emissive: palette.primary,
emissiveIntensity: 1.4,
roughness: 0.22,
}),
)
this.flower.add(core)
}

private addParticles(palette: Palette, maximum: number): THREE.Points {
private addParticles(palette: AuraColors, maximum: number): THREE.Points {
const count = Math.round(maximum * this.plan.atmosphere.particleDensity)
const positions = new Float32Array(count * 3)
const random = createSeededRandom(
Expand Down Expand Up @@ -150,7 +107,7 @@ export class AuraRenderer {
return points
}

private addAtmosphere(palette: Palette): void {
private addAtmosphere(palette: AuraColors): void {
const halo = new THREE.Mesh(
new THREE.SphereGeometry(1.35, 24, 16),
new THREE.MeshBasicMaterial({
Expand Down Expand Up @@ -192,17 +149,50 @@ export class AuraRenderer {
const pulse =
1 + Math.sin(elapsed * (0.7 + this.plan.motion.speed * 2)) * this.plan.motion.pulse * 0.12
this.flower.scale.setScalar(pulse)
this.flower.rotation.z = elapsed * this.plan.motion.rotation
this.flower.rotation.x = Math.sin(elapsed * 0.3) * this.plan.motion.turbulence * 0.16
this.petals.forEach(({ mesh, baseScale }, index) => {
const drift = 1 + Math.sin(elapsed * this.plan.motion.speed + index * 0.7) * 0.035
mesh.scale.copy(baseScale).multiplyScalar(drift)
})
this.animateGroup(elapsed)
this.formParts.forEach((part, index) => this.animatePart(part, elapsed, index))
this.particles.rotation.z = -elapsed * this.plan.motion.speed * 0.06
}
this.renderer.render(this.scene, this.camera)
}

private animateGroup(elapsed: number): void {
const family = this.plan.flower.family
if (family === 'lotus') this.flower.rotation.z = elapsed * this.plan.motion.rotation
if (family === 'crystal') this.flower.rotation.z = Math.sin(elapsed * 0.24) * 0.035
if (family === 'nebula')
this.flower.rotation.z = elapsed * (0.04 + this.plan.motion.speed * 0.08)
if (family === 'ribbon') this.flower.rotation.z = Math.sin(elapsed * 0.2) * 0.09
this.flower.rotation.x =
family === 'ribbon'
? Math.sin(elapsed * 0.32) * 0.12
: Math.sin(elapsed * 0.3) * this.plan.motion.turbulence * 0.16
}

private animatePart(part: AuraFormPart, elapsed: number, index: number): void {
const wave = Math.sin(elapsed * this.plan.motion.speed + part.phase + index * 0.13)
part.mesh.scale.copy(part.baseScale)
part.mesh.position.copy(part.basePosition)
part.mesh.rotation.copy(part.baseRotation)
if (part.motion === 'breathe') part.mesh.scale.multiplyScalar(1 + wave * 0.035)
if (part.motion === 'shard') {
part.mesh.scale.y *= 1 + wave * 0.025
part.mesh.rotation.z += wave * 0.018
}
if (part.motion === 'orbit') {
const angle = elapsed * (0.03 + this.plan.motion.speed * 0.035)
part.mesh.position.x =
part.basePosition.x * Math.cos(angle) - part.basePosition.y * Math.sin(angle)
part.mesh.position.y =
part.basePosition.x * Math.sin(angle) + part.basePosition.y * Math.cos(angle)
part.mesh.scale.multiplyScalar(1 + wave * 0.05)
}
if (part.motion === 'wave') {
part.mesh.position.y += wave * 0.055
part.mesh.rotation.x += wave * 0.035
}
}

private readonly tick = (time: number): void => {
this.animationFrame = null
if (!this.visible) return
Expand Down
Loading
Loading