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.
| Option | Default | Meaning |
|---|---|---|
renderer | required | WebGPURenderer instance |
hasCompute | true | GPU compute available (noise bake route) |
backend | "webgpu" | backend reported by createRenderer() |
width / height | window | initial framebuffer size |
preset | "sunpeek" | preset to land on |
quality | "high" | named tier or a custom march budget |
atmosphereProfile | Earth | partial 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
| Key | Sky |
|---|---|
cumulus | fair-weather puffs |
sunpeek | sun behind clouds (the hero) |
mixed | lower cumulus under high cirrus |
stratocumulus | broken low deck |
stratus | overcast ceiling |
cumulonimbus | towering storm |
cirrus | high 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.
| Tier | View | Light | Cirrus | Downscale |
|---|---|---|---|---|
performance | 40 | 4 | 4 | 3.0 |
balanced | 56 | 5 | 6 | 2.0 |
high | 72 | 6 | 8 | 1.75 |
ultra | 88 | 8 | 10 | 1.35 |
cinematic | 112 | 10 | 12 | 1.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" });