1
0

chore(feature/orb): add shader and noise maps

Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
2026-01-18 10:22:02 -05:00
parent bf2b9cbb39
commit 063ec3b7a0
2 changed files with 225 additions and 0 deletions

View File

@@ -0,0 +1,225 @@
// ============================================================================
// Orb.wgsl - Raymarched Displaced Sphere Shader
// ============================================================================
//
// A WebGPU shader that renders an animated, noise-displaced sphere using
// raymarching (sphere tracing). The sphere surface is perturbed by sampling
// a noise texture, creating an organic, fluid-like appearance.
//
// Inspired by and ported from:
// "Glazed Orb" by vochsel - https://www.shadertoy.com/view/4scSW4
//
// Techniques used:
// - Sphere tracing (raymarching) for rendering implicit surfaces
// - Signed distance functions (SDF) for geometry definition
// - Noise-based displacement for organic surface deformation
// - Fresnel reflections for realistic rim lighting
// - Environment mapping via cubemap for reflections
//
// ============================================================================
// ----------------------------------------------------------------------------
// Constants
// ----------------------------------------------------------------------------
// Surface intersection threshold - rays closer than this are considered hits
const EPS: f32 = 0.01;
// Maximum raymarching iterations to prevent infinite loops
const MAX_ITR: i32 = 100;
// Maximum ray travel distance before giving up (ray miss)
const MAX_DIS: f32 = 10.0;
// Pre-normalized light direction vector: normalize(vec3(0.0, 0.7, -2.0))
// Light comes from slightly above and in front of the sphere
const LIGHT_DIR: vec3<f32> = vec3<f32>(0.0, 0.329, -0.944);
// ----------------------------------------------------------------------------
// Uniform Data Structures
// ----------------------------------------------------------------------------
// Per-frame uniform data passed from the application
struct Uniforms {
resolution: vec2<f32>, // Viewport resolution in pixels
time: f32, // Elapsed time in seconds (drives animation)
amplitude: f32, // Displacement strength (0.0 = smooth, higher = more bumpy)
color: vec4<f32>, // Base color tint (RGB) and opacity (A)
aspectRatio: f32, // Width / Height ratio for correct projection
}
// Vertex shader output / Fragment shader input
struct VertexOutput {
@builtin(position) position: vec4<f32>, // Clip-space position
@location(0) uv: vec2<f32>, // Texture coordinates [0,1]
}
// ----------------------------------------------------------------------------
// Bindings
// ----------------------------------------------------------------------------
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var noiseTexture: texture_2d<f32>; // Displacement noise (cellular/Voronoi)
@group(0) @binding(2) var cubeTexture: texture_cube<f32>; // Environment cubemap for reflections
@group(0) @binding(3) var texSampler: sampler; // Texture sampler (linear filtering)
// ----------------------------------------------------------------------------
// Vertex Shader
// ----------------------------------------------------------------------------
// Generates a fullscreen quad from 6 vertices (2 triangles).
// Uses vertex index to determine position - no vertex buffer needed.
@vertex
fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
// Fullscreen quad vertex positions in NDC
var pos = array<vec2<f32>, 6>(
vec2<f32>(-1.0, -1.0), // Triangle 1
vec2<f32>(1.0, -1.0),
vec2<f32>(-1.0, 1.0),
vec2<f32>(-1.0, 1.0), // Triangle 2
vec2<f32>(1.0, -1.0),
vec2<f32>(1.0, 1.0)
);
var output: VertexOutput;
output.position = vec4<f32>(pos[vertexIndex], 0.0, 1.0);
output.uv = pos[vertexIndex] * 0.5 + 0.5; // Convert [-1,1] to [0,1]
return output;
}
// ----------------------------------------------------------------------------
// Signed Distance Functions (SDF)
// ----------------------------------------------------------------------------
// Returns the signed distance from point `p` to a sphere of radius `r`
// centered at the origin. Negative inside, positive outside.
fn sd_sph(p: vec3<f32>, r: f32) -> f32 {
return length(p) - r;
}
// ----------------------------------------------------------------------------
// Scene Distance Function
// ----------------------------------------------------------------------------
// Evaluates the distance field at point `p`.
// Combines a base sphere with animated noise displacement.
fn map(p: vec3<f32>) -> f32 {
// Base UV from world position (scaled down for tiling)
let u = p.xy * 0.2;
// Animated UV for large-scale displacement
// Creates slow, flowing motion across the surface
var um = u * 0.3;
um.x = um.x + uniforms.time * 0.1; // Horizontal drift
um.y = um.y - uniforms.time * 0.025; // Slower vertical drift
um.x = um.x + um.y * 2.0; // Shear for diagonal motion
// Sample noise at two frequencies:
// - hlg: Large-scale, animated features (the "goo" blobs)
// - hfn: Fine detail, static relative to surface
let hlg = textureSampleLevel(noiseTexture, texSampler, um, 0.0).x;
let hfn = textureSampleLevel(noiseTexture, texSampler, u, 0.0).x;
// Combine noise samples with amplitude control
// Fine detail is reduced where large features are prominent
let amp = max(uniforms.amplitude, 0.15);
let disp = (hlg * 0.4 + hfn * 0.1 * (1.0 - hlg)) * amp;
// Return displaced sphere distance
return sd_sph(p, 1.5) + disp;
}
// ----------------------------------------------------------------------------
// Lighting Utilities
// ----------------------------------------------------------------------------
// Schlick-style Fresnel approximation.
// Returns higher values at grazing angles (rim lighting effect).
//
// Parameters:
// bias - Minimum reflectivity at perpendicular view
// scale - Reflectivity multiplier
// power - Controls falloff sharpness
// I - Incident ray direction (normalized)
// N - Surface normal (normalized)
fn fresnel(bias: f32, scale: f32, power: f32, I: vec3<f32>, N: vec3<f32>) -> f32 {
return bias + scale * pow(1.0 + dot(I, N), power);
}
// ----------------------------------------------------------------------------
// Fragment Shader
// ----------------------------------------------------------------------------
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
// Flip Y to match expected coordinate system
let uv = vec2<f32>(input.uv.x, 1.0 - input.uv.y);
// Convert UV [0,1] to centered coordinates [-1,1] with aspect correction
var d = 1.0 - 2.0 * uv;
d.x = d.x * uniforms.aspectRatio;
// -------------------------
// Camera Setup
// -------------------------
let campos = vec3<f32>(0.0, 0.0, -2.9); // Camera position (in front of sphere)
let raydir = normalize(vec3<f32>(d.x, -d.y, 1.0)); // Ray direction per pixel
// -------------------------
// Raymarching Loop
// -------------------------
var pos = campos;
var tdist: f32 = 0.0; // Total distance traveled
var dist: f32 = EPS; // Current step distance
for (var i: i32 = 0; i < MAX_ITR; i = i + 1) {
if (dist < EPS || tdist > MAX_DIS) {
break;
}
dist = map(pos);
tdist = tdist + dist;
pos = pos + dist * raydir;
}
// -------------------------
// Shading (on hit)
// -------------------------
if (dist < EPS) {
// Compute surface normal via central differences
let eps = vec2<f32>(0.0, EPS);
let normal = normalize(vec3<f32>(
map(pos + eps.yxx) - map(pos - eps.yxx),
map(pos + eps.xyx) - map(pos - eps.xyx),
map(pos + eps.xxy) - map(pos - eps.xxy)
));
// Diffuse lighting (Lambertian)
let diffuse = max(0.0, dot(LIGHT_DIR, normal));
// Specular highlight (Blinn-Phong style, high exponent for tight highlight)
let specular = pow(diffuse, 256.0);
// Fresnel term for rim lighting / reflection intensity
let R = fresnel(0.2, 1.4, 2.0, raydir, normal);
// Environment reflection from cubemap
let reflectDir = reflect(raydir, normal);
let envReflection = textureSampleLevel(cubeTexture, texSampler, reflectDir, 0.0).rgb;
// Combine lighting components
let col = vec3<f32>(
diffuse * uniforms.color.rgb + // Base diffuse with color tint
specular * 0.1 + // Subtle specular highlight
envReflection * 0.1 + // Subtle environment reflection
R * 0.5 // Fresnel rim glow
);
// Output with premultiplied alpha
return vec4<f32>(col * uniforms.color.a, uniforms.color.a);
}
// -------------------------
// Background (ray miss)
// -------------------------
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB