Skydrift docs

A physical Three.js sky with volumetric clouds. The package ships as zero-build ES modules for Three.js r184.

Install & run

Skydrift ships as readable source. Drop the skydrift/ folder beside your page and serve it with any static file server:

cd your-project
python3 -m http.server 8099
# open http://localhost:8099/

Reference three.js and Skydrift through an import map:

<script type="importmap">
{
  "imports": {
    "three":         "https://cdn.jsdelivr.net/npm/three@0.184.0/build/three.module.js",
    "three/webgpu":  "https://cdn.jsdelivr.net/npm/three@0.184.0/build/three.webgpu.js",
    "three/tsl":     "https://cdn.jsdelivr.net/npm/three@0.184.0/build/three.tsl.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.184.0/examples/jsm/",
    "skydrift/":     "./skydrift/src/"
  }
}
</script>

Quick start

import * as THREE from "three/webgpu";
import { createRenderer } from "skydrift/renderer.js";
import { bootSkydrift }   from "skydrift/Skydrift.js";

const { renderer, backend, hasCompute } = await createRenderer();
document.body.appendChild(renderer.domElement);

const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 1, 500000);
camera.position.set(0, 2, 0);

const sky = await bootSkydrift({
  renderer, backend, hasCompute,
  preset: "mixed",
  quality: hasCompute ? "high" : "performance",
});

addEventListener("resize", () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
  sky.setSize(innerWidth, innerHeight);
});

const t0 = performance.now();
renderer.setAnimationLoop(() => {
  const t = (performance.now() - t0) / 1000;
  sky.update(camera, t);
  sky.render(renderer);
});

The cloud pass renders the atmosphere, both cloud layers, optional ground, terrain and shafts in one fullscreen composite. To add normal scene geometry, render Skydrift first, clear depth, then render the scene.

bootSkydrift(opts)

Builds every piece, including the atmosphere LUTs and cloud-noise volumes, then returns the facade. It is async, so await it.

OptionDefaultMeaning
rendererrequiredWebGPURenderer instance
hasComputetrueGPU compute available (noise bake route)
backend"webgpu"backend reported by createRenderer()
width / heightwindowinitial framebuffer size
preset"sunpeek"preset to land on
quality"high"named tier or a custom march budget
atmosphereProfileEarthpartial Rayleigh, Mie, ozone and ground profile

createSkydrift(ctx)

If you'd rather construct the pieces yourself, build them and group them with createSkydrift({ atmosphere, clouds, noise }). It returns the same facade. The facade owns the loop helpers update(), render(), setSize() and dispose().

sky: the atmosphere

sky.sky.setSun(34, 150);   // elevation°, azimuth°
sky.sky.setExposure(0.7);
sky.sky.setIntensity(7);
sky.sky.setViewHeight(2);
sky.sky.setProfile({ mieScale: 1.2, ozoneScale: 1.0 });
sky.sky.setSunAppearance({ angularRadius: 0.266, intensity: 22 });
sky.sky.setGrade({ saturation: 1.0, luminance: 1.0 });
sky.sky.getSun();           // { elevation, azimuth, direction }

clouds: shape & lighting

Every setter is chainable and returns the subsystem.

sky.clouds
  .setCoverage(0.5)
  .setDensity(12)
  .setBaseAltitude(1200)   // metres
  .setThickness(2800)
  .setErosion(0.55)
  .setVerticalDevelopment(0.4)
  .setWeather(0.65)
  .setWeatherOffset(12000, -8000)
  .setSilver(1.4)        // silver-lining strength
  .setPowder(0.48)
  .setAmbient(0.13)
  .setHaze(8e-5);        // aerial-perspective fade

cirrus: independent high layer

sky.cirrus
  .setEnabled(true)
  .setCoverage(0.42)
  .setDensity(1.7)
  .setBaseAltitude(7600)
  .setThickness(1100)
  .setShear(3000);

Cirrus has its own march, wind and erosion controls. It can run by itself or above the lower volumetric layer.

lighting: time, god rays, warmth

sky.lighting.setTime(17.5);          // 24h clock → sun arc
sky.lighting.setAzimuth(150);
sky.lighting.setGodRays(0.08);        // shaft strength, 0 = off
sky.lighting.setSunTint([1.0, 0.85, 0.64]); // golden warmth

ground & terrain

sky.ground.setEnabled(true)
  .setColor([0.035, 0.05, 0.03])
  .setHorizonClear(0.18);   // clouds below this fade to clean sky

sky.terrain.setEnabled(true)
  .setHeight(0.024)         // crest elevation, radians
  .setRoughness(0.032)
  .setHaze(0.10);          // aerial-perspective wash

presets

sky.loadPreset("cumulus");     // by name…
sky.loadPreset({ coverage: 0.6, density: 12 }); // …or an override
sky.transitionToPreset("cumulonimbus", { duration: 12 });
sky.presetNames();             // the seven keys
KeySky
cumulusfair-weather puffs
sunpeeksun behind clouds (the hero)
mixedlower cumulus under high cirrus
stratocumulusbroken low deck
stratusovercast ceiling
cumulonimbustowering storm
cirrushigh ice veil

quality

Each tier sets the view march, light cone, cirrus march, temporal feedback, and internal resolution. Switch tiers at runtime with sky.quality.set(name), or pass a custom object.

TierViewLightCirrusDownscale
performance40443.0
balanced56562.0
high72681.75
ultra888101.35
cinematic11210121.0
sky.quality.set("high");
sky.quality.set({ steps: 96, lightSteps: 8, downscale: 1.5 });

authoring & debug

sky.printSettings();   // copy-pasteable snapshot → console
sky.getSettings();     // the same object, returned
sky.getDebugData();    // per-frame probe for a HUD

Tune live, call printSettings(), and paste the result into CloudPresets.js as a new named preset.

performance

The cloud raymarch is the main cost. Resolution, coverage, occupied samples, light steps and cirrus steps all matter. WebGL2 uses a deterministic 64³ CPU noise bake and should normally begin on performance; WebGPU keeps the 128³ compute path. Start with the nearest tier, test the actual game camera and weather states, then spend more samples where the frame budget allows. cinematic is primarily an offline-capture tier.

manual wiring

For full control (sharing the renderer with a larger scene, custom bake sizes), construct the pieces directly and pass them to createSkydrift:

import { Atmosphere }       from "skydrift/Atmosphere.js";
import { CloudNoise }       from "skydrift/CloudNoise.js";
import { VolumetricClouds } from "skydrift/VolumetricClouds.js";
import { createSkydrift }   from "skydrift/Skydrift.js";

const atmosphere = new Atmosphere({ renderer });
atmosphere.bakeStatic(); atmosphere.update();
const noise = new CloudNoise({ renderer, hasCompute });
await noise.bake();
const clouds = new VolumetricClouds({ atmosphere, noise });
clouds.setSize(innerWidth, innerHeight);

const sky = createSkydrift({ atmosphere, clouds, noise, preset: "cumulus" });

See CHANGELOG for what's new and LICENSE for the terms.