How Saltline, a Three.js FPS, generates every asset in code

Saltline ships 1.4 MB of JavaScript and no model, texture or audio files. How this Three.js FPS generates geometry, PBR materials, VFX and sound in code.

Dũng Nguyễn
Nov 11, 2026
18 min read

Saltline is a first-person shooter that runs on Three.js right in the browser. The whole game fits in a single 1.39 MB JavaScript file plus 30 KB of CSS. It ships no GLB, FBX, PNG or MP3 files: the containers, the rifle, the robots, the wet asphalt, the sky and even the gunshots are all generated in code when the game boots.

This teardown walks through those procedural techniques: how the game builds geometry, bakes PBR textures on the GPU, renders rain and puddles, packs combat effects into a handful of draw calls and synthesizes audio with DSP. You can play Saltline at meowsolo.com/gameaaa before reading.

Saltline title screen: a container port in the rain, late sun breaking through the cranes, the entire scene generated in code

What Saltline is and what this analysis is based on

Saltline drops you into a container port on a rainy evening. The mission is to cut three relays of a security network that has gone rogue, then reach the boat. There are two enemy types: hounds are four-legged robots, gulls are quadrotor drones.

The original game was shared publicly as a Claude Artifact. Meow Solo recovered the bundle, kept game.js byte-for-byte identical and hosts it standalone at meowsolo.com/gameaaa. The game uses Three.js r180, WebGL2 and plain JavaScript. The analysis below is a static read of the reformatted bundle; function names are minified, so systems are described by what they do.

Measured at runtime on the high preset at 1600×900, the title screen costs about 307 draw calls, 1.87 million triangles, 137 textures and 107 shader programs. The level holds 298 props, 47 of them containers, and 96 labels packed into a single atlas.

First-person view in Saltline: the rifle between two rows of containers, wet asphalt reflecting the crane and the evening sun

Three asset mechanisms repeated in every system

Everything in Saltline goes through one of three mechanisms:

  • Geometry: assembled from hand-written primitives, with transforms and vertex colors baked in, then merged per material.
  • Materials: textures baked once on the GPU in GLSL, plugged into a MeshStandardMaterial with injected shader code.
  • Effects: ambient effects (rain, birds, blinking lights) live entirely in the vertex shader; gameplay effects (particles, shell casings, decals) run on CPU pools with fixed budgets.

Geometry: assembled from hand-written primitives

The part-list pattern and merging per material

Every builder shares the same "part list" pattern: create a primitive, bake its transform into the geometry, write vertex colors, push it into a bucket keyed by material, and finally merge each bucket into one mesh. The game has two builder classes for props, a command set for building the level, a batcher for the backdrop and a rig builder; all of them follow the same idea.

The static level is then merged once more by material.uuid, cast/receive shadow flags and layer. As a result, the draw call count tracks the number of materials rather than the number of objects. Meshes under a node flagged noMerge (fans, relays, crane spreaders) stay separate so they can still move. Props do not use InstancedMesh.

Hand-written primitives

TechniqueHow it works
UV in metersBox projection along the dominant normal axis; every primitive outputs UVs in meters, so texel density stays uniform after merging
Chamfer box24 vertices, 44 triangles; the bevel strip shares vertices with both adjacent faces, so normals interpolate on their own and edges catch highlights without extra vertices
Rounded boxBoxGeometry with 3×3×3 segments; edge and corner vertices are projected onto a rounding radius around an inner core box
LoftA 2D profile swept along a list of stations through a mapping function, with automatic winding fixes; used for container walls and barriers
Container corrugationA trapezoid profile that emits vertices only at the 4 break points of each period
DentsA seeded list of dents whose depths accumulate with a (1−d²)² falloff
LatheAnalytic normals from the profile tangent; a point is repeated to create a hard edge
ExtrudeSmooth normals when the angle between two edges is below a threshold, hard otherwise; real holes via Shape.holes, no CSG
Pipes and cablesTubeGeometry on a CatmullRomCurve3, segment count based on length
Normal weldingFace normals summed by position quantized to 0.5 mm, smoothing across UV seams

The container wall below combines the trapezoid corrugation profile, seeded dents, vertex colors that darken the grooves and a logo decal drawn with Canvas2D.

Close-up of a Saltline container wall: trapezoid corrugation, dents, rust and a shipping line logo, all generated in code

Soft shapes: tarps, sandbags, crushed cartons

Soft shapes are all subdivided rounded boxes run through a deformation function, then normal-welded.

  • Cargo tarp: a heightfield that takes the maximum of "tents" raised around each crate, plus noise and sine folds. The lashing straps are sampled from the tarp surface itself, so they always sit flush.
  • Sandbags: ends pinched by a power curve, the top puffed up, sagging per layer, plus noise.
  • Crushed carton: the top collapses with the square of height; the packing tape goes through the same deformation, so it follows the dented surface.
A cargo tarp in Saltline: the cover drapes over each crate underneath, with folds and lashing straps that follow the surface

Vertex colors as a second texture

  • AO in the corrugation grooves: 17–18% darker based on groove depth.
  • Dirt and AO when a prop is finalized: two octaves of value noise, hemispherical AO from normal.y, and a 20 cm band near the ground that is darker and browner.
_ = (0.82 + 0.18*m) * (0.92 + 0.08*A) * (0.76 + 0.24*clamp(ny*0.6 + 0.62, 0, 1));
M = clamp(1 - y/0.2, 0, 1);              // band near the ground
_ *= 1 - 0.42*M*M*(0.55 + 0.45*A);
  • Fake holes: a dark plane or disc offset by 1–2 mm instead of a boolean operation.
  • Values above 1: produce bright steel inside a dark material, folding several "materials" into one draw call; edge wear only applies to vertices on bevel faces.
  • Aerial perspective: distance haze is baked into the skyline's vertex colors.

Seeds, caching and colliders

  • Seeds: each builder has its own mulberry32 RNG; labels, colors, dents and open drawers all come from the seed.
  • Container geometry cache: the key combines name, quantized seed, door angle and wear. The container body uses a placeholder material that is remapped to paint_<color> when an instance is created, so all 8 paint colors share one geometry.
  • Colliders: hand-written AABB lists ship with each prop. Props placed off-axis are sliced into at most 10 pieces along their long axis. The same AABBs feed raycasts and the nav grid.

Level and backdrop

  • The level is procedural code with hard-coded coordinates and no data file. Container stacks get per-box position jitter, yaw and seed.
  • LOD by re-running the builder: distant cranes come from the same builder as the near crane, forced onto a single material and dropping every piece smaller than 2.6 m.
  • Ship hull: a parametric function of station, height and side; normals from central differences.
  • Distant lights: every light is an 8-triangle octahedron in one batch; a blink attribute (period, phase, duty) is evaluated in the vertex shader. Lit ship windows are thin boxes picked by hash, with no texture.
  • Birds: 12 birds in one mesh, with flight paths and wingbeats computed in the vertex shader.

Materials: PBR textures baked once on the GPU

29 texture families written in GLSL

Each of the 29 texture families is a GLSL surf(uv) function built on a shared library. The library includes a periodic PCG hash (so textures tile seamlessly), gradient noise, fbm, Voronoi F1/F2, scratches and a paint-peeling model with flaking patches, oxide halos and rust streaks running downward.

The shader runs once into a render target with 3 attachments:

o0 = vec4(sqrt(clamp(c, 0.0, 1.0)), sat(a));          // albedo + tint mask
o1 = vec4(sat(ao), clamp(r, 0.03, 1.0), sat(m), 1.0);  // AO, roughness, metalness
float hh = sat(h) * 255.0; float fl = floor(hh);
o2 = vec4(fl / 255.0, hh - fl, 0.0, 1.0);              // 16-bit height across 2 channels
  • A second pass turns height into a normal map using 8-neighbor differences; bump strength is computed from the real tile size.
  • The result is copied into a texture with mipmaps and anisotropic filtering. Size depends on quality, from 256 to 2048. Intermediate targets are destroyed after 4 seconds without use.
  • Textures are generated on demand: a family that no material has requested is never baked.

The three tiles below are the rusted-steel texture the game bakes, read straight back from the GPU at runtime: albedo, the normal map derived from height, and ORM with AO in red, roughness in green and metalness in blue.

Three outputs of a single Saltline texture bake for rusted steel: albedo, normal map and ORM with AO, roughness and metalness

Eight more families were extracted the same way: rusted steel, concrete, pallet wood, cardboard, diamond plate, chain-link, hazard stripes and galvanized steel.

Eight procedural Saltline textures baked in GLSL: rusted steel, concrete, wood, cardboard, diamond plate, chain-link, hazard stripes, galvanized steel

Albedo alpha is a tint mask

The paint shader outputs the untinted layer (rust, primer, grime) separately from how much paint remains. At render time, the paint color is added according to alpha:

diffuseColor.rgb = sampledDiffuseColor.rgb + diffuse * sampledDiffuseColor.a;

One set of paint textures yields 8 colors, while rust and grime keep their own colors. The first tile is the color channel of the container paint texture, almost black because it only holds the untinted parts. The second tile is the alpha channel, the paint that remains. The last four are composited with the formula above using four of the game's paint colors: red, blue, green and orange.

Saltline container paint texture: color channel, alpha mask, and the same texture tinted red, blue, green and orange

Patching MeshStandardMaterial with onBeforeCompile

All 48 material presets are MeshStandardMaterial. Shader code is injected through onBeforeCompile, so the game keeps Three.js shadows, IBL and BRDF intact. Each preset defines a texture family, a tint mode, rough/metal factors, a wetness parameter set and a macro-variation parameter set. The injected code adds:

  • A single ORM fetch: AO, roughness and metalness come from one texture.
  • Macro variation: 3D world-space value noise shifts albedo and roughness to break up tiling.
  • Rain wetness: strongest on upward-facing surfaces, streaky on vertical ones, a damp band near the ground and beads of water up close. Albedo gets darker and more saturated, roughness drops and normals flatten.
  • Triplanar: a variant for skinned and instanced meshes.
  • Distant chain-link: the alpha threshold drops with the mip level so wires stay about 1 pixel wide, then switches to dithering once the mesh cells are smaller than a pixel.
  • Anti-sparkle: roughness increases with how short the filtered normal-map vector is.

The rifle has its own detail shader: object-space noise for anisotropic scratches, machining rings, polymer stippling and raindrops.

Decals and labels drawn with Canvas2D

  • Decals are drawn with Canvas2D using an RNG seeded from a hash of the name.
  • Stencil lettering: draw the text, then cut a vertical gap with destination-out.
  • Wear: polygons, scratches and specks are cut out with destination-out.
  • The background is pre-filled with a very low-alpha color so mipmaps don't produce dark fringes.
  • Labels are redrawn once the webfont finishes loading.
  • Decal material: transparent, depthWrite: false, polygonOffset −2.

Every decal in the port, extracted from canvas at runtime: warning signs, shipping line logos, a CSC plate, arrows, stencil numbers, rust runs, oil stains and scorch marks. The vertical gaps in the stencil letters and the chipped patches are the destination-out cuts; the faint frame around each decal is the low-alpha background that prevents dark fringes.

Saltline Canvas2D decal set: warning signs, shipping line logos, a CSC plate, stencil lettering, numbers, rust runs, oil stains and scorch marks

Container numbers, weights, lot codes and relay names are packed into a 2048 px wide atlas with shelf packing: labels are laid out in rows by height, and a new row starts when one fills up. Each label is drawn with its edges stretched into a padding gutter so mipmaps don't bleed into the neighboring label. The level's 96 labels fill only about the top half of the atlas.

Saltline's 2048 px label atlas built with shelf packing: container numbers, weights, lot codes and relay names

Lighting, shadows and post-processing

One sky shader that doubles as the environment map

A single fragment shader on a box draws the whole sky: a 2D cloud layer from domain-warped fbm, a break in the clouds toward the sun, cloud self-shadowing from density differences along the sun direction, and lightning inside the clouds. The same shader is captured through PMREM as the environment map, with the sun disc removed and brightness clamped to avoid fireflies.

Saltline's sky drawn by one fragment shader: an fbm cloud layer, a break in the clouds and the sun behind rows of containers

PCSS sun shadows on a single shadow map

  • One DirectionalLight with one shadow map fitted to the level's AABB, no cascades. Size 1024–4096 depending on quality.
  • The getShadow function in the Three.js shader chunks is replaced with PCSS: a Vogel disk rotated by interleaved gradient noise, 8 blocker-search taps, 10–16 filter taps, penumbra width based on blocker distance, plus receiver-plane depth bias.
  • Shadows update every other frame, except on ultra.
  • 6 shared PointLights handle muzzle flashes, explosions and lights that follow objects.

A rifle that never clips through walls

A second camera with a 54° FOV (the world camera uses 74°) sees only the rifle layer. The rifle is drawn into the same HDR target with gl.depthRange(0, 0.05), so it always sits in front of everything while still getting the same post-processing as the scene.

A 9-pass post-processing chain

The scene renders into a HalfFloat target with 0/2/4× MSAA and a depth texture. Pass order:

  1. Linear depth at half resolution.
  2. 12-tap AO with normals reconstructed from depth and bilateral filtering.
  3. Volumetric raymarch with 10–40 steps that reads the sun shadow map directly; two-lobe Henyey-Greenstein phase; denser air inside the warehouse box; far range solved analytically.
  4. Bilateral upsample, then AO and fog composited into the scene.
  5. Camera motion blur via depth-based reprojection.
  6. 6-level bloom with a Karis average on the first level, plus an anamorphic streak.
  7. Auto exposure, with a sun-occlusion check that drives the lens flare.
  8. Color pass: chromatic aberration, lens dirt, flare, ACES tonemapping pulled toward AgX, split-toning, vignette and damage effects.
  9. FXAA, sharpening, film grain and dithering.

Because the volumetric pass reads the sun shadow map, light shafts through the warehouse door are clipped exactly by the door frame, and the air inside is thicker than outdoors.

Warehouse in Saltline: volumetric sunlight pouring through the large door, shelving and IBC tanks in hazy air

Quality presets and dynamic resolution

PresetScaleMSAAShadowTextureVolumetric stepsPlanarMotion blurRain
low0.70204851212offoff0.5
medium0.85220481024160.5off0.8
high1440961024240.5on1
ultra1440962048401on1

The low preset also disables AO. The game picks a preset automatically: mobile devices get low; software GPUs, Mali, Adreno, PowerVR and non-Arc Intel get medium; everything else gets high. At runtime, the resolution scale floats between 0.6 and 1.0 based on frame time, and the game drops a preset if it hits the floor twice.

Saltline Settings panel with four graphics quality levels, low, medium, high and ultra, plus mouse sensitivity and FOV

Wet ground, sea and rain

Ground data generated on the CPU

  • 80×80 m mask: R is base height with 26 hand-placed puddle dips and tire ruts, G is wetness, B is oil, A is wear along the traffic lanes.
  • Tileable detail: grain, mottling and ridged-fbm cracks, plus two slope maps in one texture.
  • Lane markings: drawn with Canvas2D in meter units.
  • The same Uint8Array behind the mask answers isPuddle and wetnessAt for gameplay, so what looks wet is exactly what gameplay treats as wet.

A wet ground shader driven by a single depth value

The ground shader is also injected into MeshStandardMaterial through onBeforeCompile. Every wetness state is derived from a single depth value:

float depth  = 0.5 - mk.r + (sn - 0.5) * 0.11 + ... + groove * 0.04 + crack * 0.02 - paint * 0.01;
float puddle = smoothstep(0.0, 0.035, depth) * uWetG;   // mirror surface
float soak   = smoothstep(-bandW, 0.0, depth) * uWetG;  // 10–30 cm soak band
float fringe = smoothstep(-0.65, -0.02, depth);         // ~1 m damp halo
  • Albedo darkens with wetness, soak and puddle depth.
  • Roughness goes from 0.74 (dry) through 0.42 (damp) and 0.2 (soaked) down to 0.055 (standing water).
  • Normals flatten inside puddles.
  • Rain ripples: each grid cell spawns one drop per cycle, across three layers of different cell sizes; beyond 10–34 m they are replaced by added roughness.
  • Expansion joints are drawn as SDFs and antialiased with fwidth.
  • Oil stains only darken and add gloss; there is no iridescence.
Wet asphalt in Saltline: puddles reflecting containers, the crane and the sky, with a damp halo spreading around their edges

Planar reflections

  • A mirror camera across the y = 0 plane with an oblique near plane.
  • A mipmapped HalfFloat target sized by the quality preset's planar factor times the resolution.
  • Blur comes from the mip chain based on roughness; UVs are distorted by the normal.
  • The result overrides the IBL radiance variable, so everything else stays the standard BRDF.
  • Sea, rain, splashes and the rifle are hidden in the reflection render.

A Gerstner-wave sea

The sea uses a custom mesh, dense near the camera and sparser with distance. The vertex shader sums 4 Gerstner waves; the fragment shader uses the analytic slopes of those 4 waves plus 7 finer waves. Foam depends on the distance to the quay wall and the ship hull's SDF. The wave formula has a CPU version so boats bob in sync with the water surface.

The sea in Saltline seen from the quay edge: Gerstner waves glittering in the evening sun, cargo ships fading on the horizon

9,000 raindrops with no CPU state

About 9,000 instanced quads, scaled by the preset's rain factor, with no state kept on the CPU. Positions are computed entirely in the vertex shader and wrapped around the camera:

p = aSeed.xyz * uBox + vel * uTime;
p = mod(p - cameraPosition + 0.5 * uBox, uBox) - 0.5 * uBox + cameraPosition;
  • Drop width has a minimum in pixels, compensated with alpha.
  • Drops are culled inside interior AABBs, except for the exact roof-hole cells the level exports.
  • Splashes read the puddle mask in the vertex shader to cluster where water collects.

Combat VFX in 5 draw calls

All combat effects fit in 5 draw calls: decals, debris, shell casings, lit particles and additive particles.

  • Procedurally generated atlases: the smoke atlas stores normals in RG, thickness in B and density in A; the additive atlas holds glow, streaks, muzzle stars, rings, fire and tracer droplets; bullet-hole decals use three canvases for color, ORM and height converted to normals.
  • Particle pools: two struct-of-arrays pools, 1,400 sorted lit particles and 3,040 unsorted additive particles. The CPU simulation covers drag, gravity, turbulence and floor collisions.
  • Quads built in the vertex shader in 3 modes: billboard, velocity-stretched and surface-aligned.
  • Smoke: soft particles read depth; lighting uses a fake sphere normal plus the atlas normal, wrap diffuse and forward scattering; sun visibility comes from one raycast per effect.
  • Tracers: 48 slots, each tracer made of 3 additive quads with a minimum pixel width.
  • Debris: an InstancedMesh of 256 distorted boxes.
  • Shell casings: an InstancedMesh of 60 casings from LatheGeometry, with quaternion integration, up to 4 bounces and an audio event per bounce.
  • Bullet holes: 160 recycled quads aligned to the surface normal, unlit.
  • Budget: fixed and not scaled by quality; the only LOD kicks in at 55 m.
Full-auto fire at a container wall in Saltline: muzzle flash, additive sparks, tracers and ejected shell casings

Rifle, hands and enemies

A procedural rifle and hands

  • Rifle: a few hundred primitives grouped into 7 buckets: 4 static ones plus the bolt, magazine and round. The receiver uses an extruded profile with holes; the barrel and suppressor use lathes; the Picatinny rail is a loop of 49 teeth.
  • Hands: lofted through rings of vertices, no skinning. Finger joints use slerped rings cut along the bisecting plane so segments never intersect. The trigger finger interpolates between two baked poses.
  • Sight: the reticle is computed from the angle off the bore axis instead of UVs, so there is no parallax.
  • Muzzle flash: 8 quads with star petals generated procedurally in the fragment shader and a new seed every shot.
Aiming down a red dot sight in Saltline: the reticle follows the angle off the bore axis, above a numbered Picatinny rail

Rifle motion from 9 springs

Sway, recoil and tilt come from 9 damped springs kicked by velocity impulses. The integrator substeps at 8 ms when a frame runs long, so the springs never blow up:

function ma(s, target, dt) {
  let n = dt > 0.012 ? Math.ceil(dt / 0.008) : 1, h = dt / n;
  for (let o = 0; o < n; o++) { s.v += ((target - s.x) * s.k - s.v * s.c) * h; s.x += s.v * h; }
  return s.x;
}

Reloads are hand-written keyframes for the left hand with smootherstep interpolation. Magazine-out, magazine-in and chambering events fire at time thresholds.

Enemy rigs and animation in code

  • Code-built rigs: bones are declared by name and parent; each geometry piece is rigidly bound to one bone with weight 1, plus AO from normal.y.
  • Instances: geometry is built once; each enemy gets its own SkinnedMesh and Skeleton sharing that geometry.
  • No AnimationMixer, no clips: controllers set bone rotation, position and scale directly every frame. Details are toggled by scaling bones down to nearly zero.
  • Hound: analytic 3-DOF leg IK; diagonal leg pairs step when the error between a planted foot and its predicted position exceeds a threshold; the body tilts with the height of all four feet through a spring.
  • Gull: flies with PD control and banks with the commanded acceleration; the rotor blur disc is just a static ring.
  • Analytic hit tests: spheres, capsules and ellipsoids; no raycasts against meshes.
The hound, Saltline's four-legged robot, standing on wet asphalt: IK-solved legs, a code-built rig and no animation clips
The gull, Saltline's quadrotor drone, flying between rows of containers; the rotor blur disc is a static ring

Audio synthesized with DSP

  • DSP library: about 20 primitives on Float32Array: white noise, RBJ biquads, time-varying biquads, exponential envelopes, modal synthesis, chirps, grains and tanh.
  • Recipes: every sound is a sum of layers. A gunshot combines crack, thump, body and mechanical noise; outdoor and indoor reverb tails are pre-baked into two stereo buffers.
  • Fixed seeds: a hash of the sound name plus a variant index, so buffers are identical every session. Playback adds round-robin and random pitch offsets.
  • Seamless loops: crossfade the tail into the head, round frequencies to the loop length and run one warm-up pass to initialize filter state.
  • Reverb: impulse responses are synthesized in code too, feeding two ConvolverNodes that crossfade by indoor zone.
  • Music: 5 stems play in sync; only their gains change with combat intensity.
  • Generation schedule: 19 buffers at boot, the rest in 6 ms slices driven by a generator, without a Worker.
  • Master: a compressor followed by tanh soft-clipping; a 45-voice pool with priorities and voice stealing.

Lessons for your own procedural Three.js game

You don't need to copy all of Saltline to benefit from how it is organized. These eight principles apply to most small web games:

  1. Merge by material, not by object. Bake transforms into geometry, merge each bucket into one mesh and keep only moving parts separate. Draw calls then track the number of materials.
  2. Bake textures once instead of every frame. Write surf(uv) in GLSL, render it into a multi-attachment render target, copy it into a mipmapped texture, and only bake the families in use.
  3. Use alpha as a tint mask. One set of paint textures serves many colors, with no per-color texture copies.
  4. Patch MeshStandardMaterial instead of writing shaders from scratch. onBeforeCompile lets you add wetness, macro variation and triplanar mapping while keeping Three.js shadows, IBL and BRDF.
  5. Push ambient effects into the vertex shader. Rain, birds and blinking lights need no CPU state; only effects that touch gameplay run on CPU pools.
  6. Cap your VFX budget. Particle pools, tracer slots and bullet holes all have limits and recycle when full, so frame time doesn't balloon with the size of a firefight.
  7. Seed everything. Seeded RNGs for props, decals and audio make every run identical, which makes debugging and caching easier.
  8. Measure frame time and adapt. Pick a preset from the GPU at boot, then scale resolution with frame time and drop a preset when needed.

Scope and limitations of this analysis

  • This is a static read of the reformatted bundle; the game itself was not modified.
  • Materials, decals, sky, shadows, post-processing and the renderer were read directly. Props, level, ground, FX, rifle, enemies and audio were read in parallel by several AI agents, with spot checks against the code in each part rather than line by line.
  • Triangle counts per asset were not measured; the totals at the top of this post were measured at runtime.
  • That puddle and sea Fresnel goes through the standard Three.js BRDF is inferred from the shader structure and has not been checked against the r180 shader chunks.
  • Screenshots come from the build at meowsolo.com/gameaaa on the high preset, with the camera positioned in code to frame each shot. Textures, decals and the label atlas were extracted directly from the GPU and canvases at runtime; the four tinted paint tiles were composited with the tint formula.

FAQ

Does Saltline use any model or texture files?

No. The bundle ships no GLB, FBX, image or audio files. Models, textures, effects and audio are all generated at runtime; the only extra download is the webfont.

Which Three.js version does Saltline use?

Three.js r180 on WebGL2, written in plain JavaScript. The build at meowsolo.com/gameaaa needs no build step and no installed libraries.

Should a small Three.js game generate its assets in code?

Generating assets in code keeps the bundle small and makes variations cheap, at the cost of startup time and the effort of writing shaders. Saltline strikes the balance by baking once, caching by seed, generating only what is in use and slicing audio synthesis into 6 ms chunks so it never blocks a frame.

To check for yourself, open Saltline, switch between the four quality presets in Settings and stand next to a puddle while it rains. Meow Solo also makes two other 3D games that run in the browser: Mellow Miles, a run around Hoan Kiem Lake, and Tiệm Cà Phê Bất Ổn, a café management game.

Three.js
Procedural generation
WebGL
GLSL
Browser games
Game development
See Meow Solo products
All posts

Have a problem that needs solving?

Share the problem and the result you’re after, and we’ll find a way of working together that benefits both sides.
Get in touch
Or email directly
mrdee0428@gmail.com
Facebook
Telegram