133 lines
5.2 KiB
TypeScript
133 lines
5.2 KiB
TypeScript
import type { Color, Material, Side, Texture } from 'three'
|
|
import { cos, Fn, float, instanceIndex, positionLocal, sin, time, uv } from 'three/tsl'
|
|
import { MeshStandardNodeMaterial } from 'three/webgpu'
|
|
|
|
/**
|
|
* Always-on wind for the plant kinds, done in TSL so it runs on the editor's
|
|
* WebGPU renderer (which ignores WebGL's `onBeforeCompile`). Two motions:
|
|
*
|
|
* - **Leaf flutter** (`LEAF_FLUTTER`) — ez-tree's own approach: the sway scales
|
|
* with the leaf card's `uv.y`, so each leaf swings from its attachment while
|
|
* the trunk and branches stay put. A whole-tree height-based bend, by contrast,
|
|
* is a rigid rotation about the base and reads as the tree spinning in place.
|
|
* Multi-frequency for a natural gust; phased per instance + per leaf.
|
|
* - **Stem bend** (`STEM_BEND`) — for the small procedural kinds (flowers,
|
|
* grass), a gentle whole-plant lean proportional to height reads fine.
|
|
*
|
|
* ez-tree isn't touched: its generated materials are re-created as node materials
|
|
* carrying the texture/tint (`toWindMaterial`) — only the `leaves` material gets
|
|
* the flutter node; bark stays static. `time` is advanced by the renderer.
|
|
*/
|
|
|
|
// ── Leaf flutter (tree leaves) ───────────────────────────────────────────────
|
|
const LEAF_FREQUENCY = 1.2
|
|
const LEAF_STRENGTH = 0.3
|
|
|
|
const leafFlutter = Fn(() => {
|
|
const p = positionLocal.toVar()
|
|
const offset = float(instanceIndex).mul(0.7).add(p.x.add(p.z).mul(0.3))
|
|
const t = time.mul(LEAF_FREQUENCY)
|
|
const wave = sin(t.add(offset))
|
|
.mul(0.5)
|
|
.add(sin(t.mul(2).add(offset.mul(1.3))).mul(0.3))
|
|
.add(sin(t.mul(5).add(offset.mul(1.5))).mul(0.2))
|
|
const sway = uv().y.mul(LEAF_STRENGTH).mul(wave)
|
|
p.x.addAssign(sway)
|
|
p.z.addAssign(sway)
|
|
return p
|
|
})
|
|
const LEAF_FLUTTER = leafFlutter()
|
|
|
|
// ── Stem bend (flowers, grass) ───────────────────────────────────────────────
|
|
const STEM_FREQUENCY = 1.3
|
|
const STEM_STRENGTH = 0.05
|
|
|
|
const stemBend = Fn(() => {
|
|
const p = positionLocal.toVar()
|
|
const h = p.y.max(0)
|
|
const phase = float(instanceIndex).mul(0.618)
|
|
const t = time.mul(STEM_FREQUENCY).add(phase)
|
|
p.x.addAssign(h.mul(STEM_STRENGTH).mul(sin(t)))
|
|
p.z.addAssign(h.mul(STEM_STRENGTH).mul(cos(t.mul(1.1))))
|
|
return p
|
|
})
|
|
const STEM_BEND = stemBend()
|
|
|
|
/** The classic-material fields we carry over — enough to reproduce ez-tree's
|
|
* bark/leaf look (textured, tinted, alpha-cut billboards). */
|
|
type ClassicMaterial = Material & {
|
|
map?: Texture | null
|
|
alphaMap?: Texture | null
|
|
color?: Color
|
|
side?: Side
|
|
alphaTest?: number
|
|
opacity?: number
|
|
transparent?: boolean
|
|
depthWrite?: boolean
|
|
}
|
|
|
|
const cache = new WeakMap<Material, MeshStandardNodeMaterial>()
|
|
|
|
/** Re-create a generated (ez-tree) material as a `MeshStandardNodeMaterial`,
|
|
* transferring its texture/tint explicitly (node materials don't pick these up
|
|
* via `Material.copy()`). Only the `leaves` material flutters; bark stays static.
|
|
* Cached per source so shared variant materials convert once. */
|
|
export function toWindMaterial(material: Material): MeshStandardNodeMaterial {
|
|
const cached = cache.get(material)
|
|
if (cached) return cached
|
|
const src = material as ClassicMaterial
|
|
const node = new MeshStandardNodeMaterial({
|
|
map: src.map ?? null,
|
|
alphaMap: src.alphaMap ?? null,
|
|
color: src.color,
|
|
side: src.side,
|
|
alphaTest: src.alphaTest ?? 0,
|
|
transparent: src.transparent ?? false,
|
|
opacity: src.opacity ?? 1,
|
|
depthWrite: src.depthWrite ?? true,
|
|
roughness: 1,
|
|
metalness: 0,
|
|
})
|
|
if (material.name === 'leaves') node.positionNode = LEAF_FLUTTER
|
|
cache.set(material, node)
|
|
return node
|
|
}
|
|
|
|
/** Build a swaying node material for the procedural kinds (flowers/grass). */
|
|
export function windStandardMaterial(
|
|
params: ConstructorParameters<typeof MeshStandardNodeMaterial>[0],
|
|
): MeshStandardNodeMaterial {
|
|
const material = new MeshStandardNodeMaterial(params)
|
|
material.positionNode = STEM_BEND
|
|
return material
|
|
}
|
|
|
|
const staticCache = new WeakMap<Material, Material>()
|
|
|
|
/** Windless twin of a wind material — same look, no `positionNode`. The outline
|
|
* mask pass renders outlined meshes with a shared override material, so an
|
|
* outline can never follow the GPU sway; the selection proxy renders this twin
|
|
* instead, so the outlined silhouette and the visible mesh match exactly (the
|
|
* plant simply holds still while hovered/selected). Built by explicit property
|
|
* transfer, not `.clone()` — node-material clone drops `map`/`color` (same
|
|
* pitfall as `toWindMaterial`). Cached per source. */
|
|
export function toStaticMaterial(material: Material): Material {
|
|
const cached = staticCache.get(material)
|
|
if (cached) return cached
|
|
const src = material as MeshStandardNodeMaterial
|
|
const twin = new MeshStandardNodeMaterial({
|
|
map: src.map ?? null,
|
|
alphaMap: src.alphaMap ?? null,
|
|
color: src.color,
|
|
side: src.side,
|
|
alphaTest: src.alphaTest ?? 0,
|
|
transparent: src.transparent ?? false,
|
|
opacity: src.opacity ?? 1,
|
|
depthWrite: src.depthWrite ?? true,
|
|
roughness: src.roughness,
|
|
metalness: src.metalness,
|
|
})
|
|
staticCache.set(material, twin)
|
|
return twin
|
|
}
|