feat: fancy clouds, fps meter
This commit is contained in:
parent
b0878ad5e1
commit
2c31bd82bf
5 changed files with 120 additions and 26 deletions
|
|
@ -1,2 +1,4 @@
|
||||||
# MEAT
|
# MEAT
|
||||||
|
|
||||||
|
- knobs
|
||||||
|
- toggle clouds: off, basic, fancy
|
||||||
|
|
|
||||||
33
app/level.ts
33
app/level.ts
|
|
@ -1,5 +1,5 @@
|
||||||
import { Color } from "../engine/render/Color"
|
import { Color } from "../engine/render/Color"
|
||||||
import type { SkyConfig } from "../engine/render/Sky"
|
import type { CloudLayer, SkyConfig } from "../engine/render/Sky"
|
||||||
import type { Mesh } from "../engine/scene/Mesh"
|
import type { Mesh } from "../engine/scene/Mesh"
|
||||||
|
|
||||||
type Corner = [number, number, number]
|
type Corner = [number, number, number]
|
||||||
|
|
@ -40,6 +40,29 @@ const CRATE = { x: -2, z: -2, half: 1, top: 1 }
|
||||||
* from the render side. */
|
* from the render side. */
|
||||||
const DIVISIONS_PER_UNIT = 1.3
|
const DIVISIONS_PER_UNIT = 1.3
|
||||||
|
|
||||||
|
/** The two cloud styles; swap which one the sky uses in `buildLevel`.
|
||||||
|
* `basicCumulus` is cheap flat puffs; `fancyCumulus` is the pricier
|
||||||
|
* heightfield-shaded, domain-warped version with faked volume. */
|
||||||
|
export const basicCumulus: CloudLayer = {
|
||||||
|
kind: "basic",
|
||||||
|
color: Color.rgb(248, 250, 255),
|
||||||
|
coverage: 0.5,
|
||||||
|
scale: 0.9,
|
||||||
|
speed: 0.5,
|
||||||
|
edge: 0.02,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fancyCumulus: CloudLayer = {
|
||||||
|
kind: "fancy",
|
||||||
|
color: Color.rgb(250, 251, 255),
|
||||||
|
coverage: 0.5,
|
||||||
|
scale: 0.6,
|
||||||
|
speed: 0.5,
|
||||||
|
edge: 0.02,
|
||||||
|
warp: 0.4,
|
||||||
|
relief: 7,
|
||||||
|
}
|
||||||
|
|
||||||
export function buildLevel(): Level {
|
export function buildLevel(): Level {
|
||||||
const floor = mesh()
|
const floor = mesh()
|
||||||
quadGrid(floor, [-ARENA, 0, -ARENA], [ARENA, 0, -ARENA], [ARENA, 0, ARENA], [-ARENA, 0, ARENA], 12, 12)
|
quadGrid(floor, [-ARENA, 0, -ARENA], [ARENA, 0, -ARENA], [ARENA, 0, ARENA], [-ARENA, 0, ARENA], 12, 12)
|
||||||
|
|
@ -76,13 +99,7 @@ export function buildLevel(): Level {
|
||||||
sun: Color.rgb(255, 246, 214),
|
sun: Color.rgb(255, 246, 214),
|
||||||
sunDir: { x: 0.3, y: 0.5, z: -0.8 },
|
sunDir: { x: 0.3, y: 0.5, z: -0.8 },
|
||||||
sunSize: 0.04,
|
sunSize: 0.04,
|
||||||
clouds: {
|
clouds: fancyCumulus,
|
||||||
color: Color.rgb(248, 250, 255),
|
|
||||||
coverage: 0.5,
|
|
||||||
scale: 0.9,
|
|
||||||
speed: 0.5,
|
|
||||||
edge: 0.02,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { floor, walls, crate, colliders, npcPosition: { x: 2, y: 0, z: -1 }, sky }
|
return { floor, walls, crate, colliders, npcPosition: { x: 2, y: 0, z: -1 }, sky }
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const screen = document.querySelector<HTMLCanvasElement>("#screen")!
|
||||||
const ctx = screen.getContext("2d")!
|
const ctx = screen.getContext("2d")!
|
||||||
const back = document.createElement("canvas")
|
const back = document.createElement("canvas")
|
||||||
const backCtx = back.getContext("2d")!
|
const backCtx = back.getContext("2d")!
|
||||||
|
const fpsEl = document.querySelector<HTMLDivElement>("#fps")!
|
||||||
|
|
||||||
let config: RenderConfig = RenderConfig.standard
|
let config: RenderConfig = RenderConfig.standard
|
||||||
let fb = Framebuffer.create(1, 1)
|
let fb = Framebuffer.create(1, 1)
|
||||||
|
|
@ -82,9 +83,17 @@ async function main(): Promise<void> {
|
||||||
resize()
|
resize()
|
||||||
|
|
||||||
let last = performance.now()
|
let last = performance.now()
|
||||||
|
let fpsLast = last
|
||||||
|
let fpsFrames = 0
|
||||||
function frame(now: number): void {
|
function frame(now: number): void {
|
||||||
const dt = Math.min(0.05, (now - last) / 1000)
|
const dt = Math.min(0.05, (now - last) / 1000)
|
||||||
last = now
|
last = now
|
||||||
|
fpsFrames++
|
||||||
|
if (now - fpsLast >= 250) {
|
||||||
|
fpsEl.textContent = `${Math.round((fpsFrames * 1000) / (now - fpsLast))} fps`
|
||||||
|
fpsLast = now
|
||||||
|
fpsFrames = 0
|
||||||
|
}
|
||||||
Player.update(player, keys, dt, level)
|
Player.update(player, keys, dt, level)
|
||||||
|
|
||||||
const camera: Camera = {
|
const camera: Camera = {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@ import type { Framebuffer } from "./Framebuffer"
|
||||||
import { Camera } from "../scene/Camera"
|
import { Camera } from "../scene/Camera"
|
||||||
import { Vec3 } from "../math/Vec3"
|
import { Vec3 } from "../math/Vec3"
|
||||||
|
|
||||||
/** One procedural cloud layer. For now a single cumulus type; add more kinds
|
/** Fields shared by every cumulus style. */
|
||||||
* later by giving this a `kind` field and branching in the cloud shader. */
|
export type CumulusBase = {
|
||||||
export type CloudLayer = {
|
|
||||||
color: Color
|
color: Color
|
||||||
/** Roughly the fraction of sky covered, 0..1. */
|
/** Roughly the fraction of sky covered, 0..1. */
|
||||||
coverage: number
|
coverage: number
|
||||||
|
|
@ -17,6 +16,25 @@ export type CloudLayer = {
|
||||||
edge: number
|
edge: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Flat, hard-thresholded white cumulus. Cheap: one noise lookup per pixel. */
|
||||||
|
export type BasicCumulus = CumulusBase & { kind: "basic" }
|
||||||
|
|
||||||
|
/** Domain-warped, heightfield-shaded cumulus with faked volume. Pricier
|
||||||
|
* (~5 noise lookups per pixel) but reads as billowing 3D puffs. */
|
||||||
|
export type FancyCumulus = CumulusBase & {
|
||||||
|
kind: "fancy"
|
||||||
|
/** Domain-warp amount: bends the noise into bulbous, cauliflower puffs.
|
||||||
|
* 0 = round blobs, higher = more billowing. */
|
||||||
|
warp: number
|
||||||
|
/** Fake vertical relief for lighting: 0 = flat, higher = deeper, more
|
||||||
|
* three-dimensional bulges (bright sun-side, shaded underside). */
|
||||||
|
relief: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One procedural cloud layer. Add more styles by extending this union and
|
||||||
|
* branching on `kind` in the cloud shader. */
|
||||||
|
export type CloudLayer = BasicCumulus | FancyCumulus
|
||||||
|
|
||||||
/** Procedural sky: a vertical gradient, a sun disc, and optional moving clouds. */
|
/** Procedural sky: a vertical gradient, a sun disc, and optional moving clouds. */
|
||||||
export type SkyConfig = {
|
export type SkyConfig = {
|
||||||
zenith: Color
|
zenith: Color
|
||||||
|
|
@ -51,6 +69,7 @@ export namespace Sky {
|
||||||
const sun = Vec3.normalize(sky.sunDir)
|
const sun = Vec3.normalize(sky.sunDir)
|
||||||
const cosSun = Math.cos(sky.sunSize)
|
const cosSun = Math.cos(sky.sunSize)
|
||||||
const clouds = sky.clouds
|
const clouds = sky.clouds
|
||||||
|
const cloud: CloudSample = { cover: 0, shade: 1 }
|
||||||
for (let y = 0; y < height; y++) {
|
for (let y = 0; y < height; y++) {
|
||||||
const ndcY = 1 - ((y + 0.5) / height) * 2
|
const ndcY = 1 - ((y + 0.5) / height) * 2
|
||||||
for (let x = 0; x < width; x++) {
|
for (let x = 0; x < width; x++) {
|
||||||
|
|
@ -72,9 +91,13 @@ export namespace Sky {
|
||||||
c = Color.lerp(c, sky.sun, glow)
|
c = Color.lerp(c, sky.sun, glow)
|
||||||
}
|
}
|
||||||
if (clouds !== null && dy > 0.02) {
|
if (clouds !== null && dy > 0.02) {
|
||||||
const cover = cumulus(dx, dy, dz, clouds, time)
|
if (clouds.kind === "fancy") {
|
||||||
if (cover > 0) {
|
fancyCumulus(dx, dy, dz, clouds, time, sun, cloud)
|
||||||
c = Color.lerp(c, clouds.color, cover)
|
} else {
|
||||||
|
basicCumulus(dx, dy, dz, clouds, time, cloud)
|
||||||
|
}
|
||||||
|
if (cloud.cover > 0) {
|
||||||
|
c = Color.lerp(c, Color.scale(clouds.color, cloud.shade), cloud.cover)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const i = y * width + x
|
const i = y * width + x
|
||||||
|
|
@ -84,20 +107,51 @@ export namespace Sky {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Reusable per-pixel cloud result, to avoid allocating in the sky loop. */
|
||||||
* Coverage in 0..1 of a cumulus layer along a view ray. The ray is projected
|
type CloudSample = { cover: number; shade: number }
|
||||||
* onto a flat cloud plane "at infinity" (xz / y), scrolled by wind, sampled
|
|
||||||
* with fractal noise, then hard-thresholded so the clouds have distinct puffy
|
/** Hard-threshold a noise density into cloud coverage (distinct puffy edges),
|
||||||
* edges rather than a foggy falloff. Fades out near the horizon, where the
|
* then fade it out near the horizon where the cloud-plane projection breaks
|
||||||
* projection blows up into noise.
|
* down. Shared by both cumulus styles. */
|
||||||
*/
|
function coverage(dy: number, layer: CumulusBase, density: number): number {
|
||||||
function cumulus(dx: number, dy: number, dz: number, layer: CloudLayer, time: number): number {
|
const threshold = 0.72 - layer.coverage * 0.4
|
||||||
|
return smoothstep(threshold - layer.edge, threshold + layer.edge, density) * smoothstep(0.02, 0.22, dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** basicCumulus -- flat, hard-thresholded white puffs, no lighting. Cheap:
|
||||||
|
* one noise lookup per pixel. `shade` stays 1 (uniform white). */
|
||||||
|
function basicCumulus(dx: number, dy: number, dz: number, layer: CloudLayer, time: number, out: CloudSample): void {
|
||||||
const u = (dx / dy) * layer.scale + time * layer.speed
|
const u = (dx / dy) * layer.scale + time * layer.speed
|
||||||
const v = (dz / dy) * layer.scale
|
const v = (dz / dy) * layer.scale
|
||||||
const density = fbm(u, v)
|
out.cover = coverage(dy, layer, fbm(u, v))
|
||||||
const threshold = 0.72 - layer.coverage * 0.4
|
out.shade = 1
|
||||||
const cover = smoothstep(threshold - layer.edge, threshold + layer.edge, density)
|
}
|
||||||
return cover * smoothstep(0.02, 0.22, dy)
|
|
||||||
|
/** fancyCumulus -- domain-warped clouds with faked volume. The noise doubles
|
||||||
|
* as a heightfield whose gradient is a fake surface normal, lit so up-facing
|
||||||
|
* tops read bright and steep bulge sides shade into shadow (the sun picks the
|
||||||
|
* lit side). ~5 noise lookups per pixel, so noticeably pricier. */
|
||||||
|
function fancyCumulus(dx: number, dy: number, dz: number, layer: FancyCumulus, time: number, sun: Vec3, out: CloudSample): void {
|
||||||
|
const u = (dx / dy) * layer.scale + time * layer.speed
|
||||||
|
const v = (dz / dy) * layer.scale
|
||||||
|
// Domain warp: nudge the sample point by another noise field for bulges.
|
||||||
|
const wu = u + layer.warp * fbm(u * 0.5 + 5.2, v * 0.5 + 1.3)
|
||||||
|
const wv = v + layer.warp * fbm(u * 0.5 + 9.1, v * 0.5 + 4.7)
|
||||||
|
const density = fbm(wu, wv)
|
||||||
|
out.cover = coverage(dy, layer, density)
|
||||||
|
if (out.cover <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Treat density as height; its gradient is a fake surface normal (nx, ny, 1).
|
||||||
|
const e = 0.15
|
||||||
|
const nx = -(fbm(wu + e, wv) - density) * layer.relief
|
||||||
|
const ny = -(fbm(wu, wv + e) - density) * layer.relief
|
||||||
|
const inv = 1 / Math.hypot(nx, ny, 1)
|
||||||
|
// Up-facing tops read bright; steep bulge sides fall into shadow, and the
|
||||||
|
// sun (mapped x -> u, z -> v, y -> up) picks out the lit side.
|
||||||
|
const up = inv
|
||||||
|
const sunFace = Math.max(0, (nx * sun.x + ny * sun.z + sun.y) * inv)
|
||||||
|
out.shade = Math.min(1, 0.4 + 0.35 * up + 0.35 * sunFace)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fractal (value-noise) sum, ~0..1, giving lumpy cumulus shapes. */
|
/** Fractal (value-noise) sum, ~0..1, giving lumpy cumulus shapes. */
|
||||||
|
|
|
||||||
12
index.html
12
index.html
|
|
@ -16,10 +16,22 @@
|
||||||
display: block;
|
display: block;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
#fps {
|
||||||
|
position: fixed;
|
||||||
|
right: 6px;
|
||||||
|
bottom: 6px;
|
||||||
|
font: 16px monospace;
|
||||||
|
color: #fff;
|
||||||
|
background: #000;
|
||||||
|
opacity: 0.5;
|
||||||
|
padding: 1px 5px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="screen"></canvas>
|
<canvas id="screen"></canvas>
|
||||||
|
<div id="fps">-- fps</div>
|
||||||
<script type="module" src="/app/main.ts"></script>
|
<script type="module" src="/app/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue