diff --git a/feature/orb/src/main/assets/shaders/Orb.wgsl b/feature/orb/src/main/assets/shaders/Orb.wgsl new file mode 100644 index 0000000..b112db9 --- /dev/null +++ b/feature/orb/src/main/assets/shaders/Orb.wgsl @@ -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 = vec3(0.0, 0.329, -0.944); + +// ---------------------------------------------------------------------------- +// Uniform Data Structures +// ---------------------------------------------------------------------------- + +// Per-frame uniform data passed from the application +struct Uniforms { + resolution: vec2, // Viewport resolution in pixels + time: f32, // Elapsed time in seconds (drives animation) + amplitude: f32, // Displacement strength (0.0 = smooth, higher = more bumpy) + color: vec4, // 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, // Clip-space position + @location(0) uv: vec2, // Texture coordinates [0,1] +} + +// ---------------------------------------------------------------------------- +// Bindings +// ---------------------------------------------------------------------------- + +@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(1) var noiseTexture: texture_2d; // Displacement noise (cellular/Voronoi) +@group(0) @binding(2) var cubeTexture: texture_cube; // 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, 6>( + vec2(-1.0, -1.0), // Triangle 1 + vec2(1.0, -1.0), + vec2(-1.0, 1.0), + vec2(-1.0, 1.0), // Triangle 2 + vec2(1.0, -1.0), + vec2(1.0, 1.0) + ); + + var output: VertexOutput; + output.position = vec4(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, 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 { + // 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, N: vec3) -> f32 { + return bias + scale * pow(1.0 + dot(I, N), power); +} + +// ---------------------------------------------------------------------------- +// Fragment Shader +// ---------------------------------------------------------------------------- + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + // Flip Y to match expected coordinate system + let uv = vec2(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(0.0, 0.0, -2.9); // Camera position (in front of sphere) + let raydir = normalize(vec3(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(0.0, EPS); + let normal = normalize(vec3( + 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( + 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(col * uniforms.color.a, uniforms.color.a); + } + + // ------------------------- + // Background (ray miss) + // ------------------------- + return vec4(0.0, 0.0, 0.0, 0.0); +} diff --git a/feature/orb/src/main/assets/textures/noise_map.png b/feature/orb/src/main/assets/textures/noise_map.png new file mode 100644 index 0000000..5f05401 Binary files /dev/null and b/feature/orb/src/main/assets/textures/noise_map.png differ