www.xbdev.net
xbdev - software development
Wednesday May 7, 2025
Home | Contact | Support | Programming.. More than just code .... | Computer Graphics Powerful and Beautiful ...
     
 

Computer Graphics

Powerful and Beautiful ...

 

Generating fun little planets using noise functions, signed distance functions, ray-tracing, ray-marching and some creative fun.
Generating fun little planets using noise functions, signed distance functions, ray-tracing, ray-marching and some creative fun.


Little Planets


The fun of using random noise, signed distance functions and ray-marching to create small planets.


Example of the type of 3d generated planet that can be created using ray-tracing, ray-marching and fractal noise.
Example of the type of 3d generated planet that can be created using ray-tracing, ray-marching and fractal noise.


The implementation will run on the GPU (webgpu) - so it runs real-time in a web-browser. The great thing about running it in a web-browser is it's easy for you to try out and experiment with the result.

The links with working source code are available at the end - so you can open them up in an editor and try them out. The following will take you through the steps - so you can see who the implementation works - as you can sometimes get overwhelmed when you're presented with a complete working piece of code.

The implementation mostly happens on the fragment shader - we use 2d in the beginning to explain the sdf operations - before switching over to a 3d ray-tracer.

Start with Signed Distance Function (Sphere)


We'll start simple by using 2d signed function visualizations to take you through the process of building the planet and cloud shapes (with noise).

As a starting point - we'll implement a fragment shader that draws an sdf shape on screen for a cube and sphere combined together (union).


Simple SDF that shows a 2d cross section of a sphere and a cube (union).
Simple SDF that shows a 2d cross section of a sphere and a cube (union).


fn sdfSpheretestPoint:vec3<f32>,  spherePos:vec3<f32>,  sphereRadius:f32 )  ->f32
{
    return 
length(spherePos testPoint) - sphereRadius;
}

fn 
sdfCubetestPoint:vec3<f32>,  cubePos:vec3<f32>,  cubeDim:vec3<f32> ) ->f32
{
   var 
d:vec3<f32> = abs(cubePos testPoint) - cubeDim;
   return 
min(max(d.xmax(d.yd.z)), 0.0)
           + 
lengthmax(dvec3<f32>(0.0) ) );
}

// Sdf scene for a sphere and cube union
fn sdfScenept:vec3<f32> ) -> f32 
{
    var 
t:f32 0.0;
    
maxtsdfCube(ptvec3<f32>( 0.000), vec3<f32>(1.0)) );
    
mintsdfSphere(ptvec3<f32>(0.0,0,0), 1.1 ) );
    return 
t;
}

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let zoom 2.0;
    
let nuv = ( uv 2.0 1.0 ) * zoom;
 
    
let point vec3nuv 0.0 );
    
    
let d sdfScenepoint );

    var 
color vec3abs(sin(d*40.0)) );

    if ( 
abs(d) < 0.01 )
    {
        
color vec3<f32>(0.00.80.0);
    }
    
// if inside - use red rings for the gradients
    
if ( 0.0 )
    {
         
color *= vec3<f32>(1.00.00.0);   
    }
    return 
vec4<f32>(color1.0); 
}


Simple SDF Noise Function


We can write an sdf function for noise - call it
sdfNoise(..)
. For testing, we'll just add the noise to the sphere (union).


Adding noise to the sdf scene.
Adding noise to the sdf scene.


fn random(uvvec2<f32>) -> f32 {
    return 
fract(sin(dot(uvvec2<f32>(12.989878.233))) * 43758.5453);
}

fn 
noise(posvec3<f32>) -> f32 {
    
let p floor(pos*2.0); // Integer part of the position
    
let f fract(pos*2.0); // Fractional part of the position

    // Smooth interpolation factor
    
let f2 * (3.0 2.0 f);

    
// Generate random values at the corners of the cube
    
let n000 random(p.xy vec2<f32>(0.0p.z));
    
let n001 random(p.xy vec2<f32>(0.0p.1.0));
    
let n010 random(p.xy vec2<f32>(1.0p.z));
    
let n011 random(p.xy vec2<f32>(1.0p.1.0));
    
let n100 random(p.xy vec2<f32>(0.0p.1.0));
    
let n101 random(p.xy vec2<f32>(0.0p.2.0));
    
let n110 random(p.xy vec2<f32>(1.0p.1.0));
    
let n111 random(p.xy vec2<f32>(1.0p.2.0));

    
// Interpolate along the x-axis
    
let nx00 mix(n000n010f2.x);
    
let nx01 mix(n001n011f2.x);
    
let nx10 mix(n100n110f2.x);
    
let nx11 mix(n101n111f2.x);

    
// Interpolate along the y-axis
    
let nxy0 mix(nx00nx10f2.y);
    
let nxy1 mix(nx01nx11f2.y);

    
// Interpolate along the z-axis
    
return mix(nxy0nxy1f2.z);
}

fn 
sdfNoise(ptvec3<f32>, thresholdf32) -> f32 {
    
// 1. Get noise value at this point (0-1 range)
    
let noise_val fbm(pt); // Adjust scale as needed
    
    // 2. Convert to signed distance:
    // Negative inside noise (density > threshold)
    // Positive outside noise (density < threshold)
    
return threshold noise_val;
}

fn 
sdfSpheretestPoint:vec3<f32>,  spherePos:vec3<f32>,  sphereRadius:f32 )  ->f32
{
    return 
length(spherePos testPoint) - sphereRadius;
}

// add together
fn sdfUnion(a:f32b:f32) -> f32 {
    return 
min(ab);
}

// Sdf scene 
fn sdfScenept:vec3<f32> ) -> f32 
{
    
let d0:f32 sdfNoise(pt0.5);
    
let d1:f32 sdfSphere(ptvec3<f32>(0.0,0,0), 1.1 );
    
let d sdfUniond0d1 );
    return 
d;
}

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let zoom 2.0;
    
let nuv = ( uv 2.0 1.0 ) * zoom;
 
    
let point vec3nuv 0.0 );
    
    
let d sdfScenepoint );

    var 
color vec3abs(sin(d*40.0)) );

    if ( 
abs(d) < 0.01 )
    {
        
color vec3<f32>(0.00.80.0);
    }
    
// if inside - use red rings for the gradients
    
if ( 0.0 )
    {
         
color *= vec3<f32>(1.00.00.0);   
    }
    return 
vec4<f32>(color1.0); 
}


Add More 'Combination' Operations


As well as addition using the union operation - we can do
difference
,
intersection
and so on. We'll add these functions so that we can create more complex scenes. We'll also include the soft version - which let us 'smooth' the operation (blends).

// add together
fn sdfUnion(a:f32b:f32) -> f32 {
    return 
min(ab);
}

// Subtract one shape from the other
fn sdfDifference(af32bf32) -> f32 {
    return 
max(a, -b);
}

// Keep only the overlapping intersection parts
fn sdfIntersection(af32bf32) -> f32 {
    return 
max(ab);
}

// Keeps the parts that are NOT overlapping
fn sdfXORDifference(af32bf32) -> f32 {
    return 
max(min(ab), min(-a, -b));
}

// ---------------------------------
// Smooth union operations
fn smoothunion(d1f32d2f32kf32) -> f32 {
    
let h max(abs(d1 d2), 0.0) / k;

    return 
min(d1d2) - 0.25;
}

// Subtract one shape from the other
fn smoothdifference(d1f32d2f32kf32) -> f32 {
    
let h max(abs(d1 d2), 0.0) / k;
    
    return 
max(d1, -d2) - 0.25;
}

// Keep only the overlapping intersection parts
fn smoothintersection(d1f32d2f32kf32) -> f32 {
    
let h max(abs(d1 d2), 0.0) / k;
    
    return 
max(d1d2) + 0.25;
}


Water Surface and Noise


We are going to combine two spheres - one with noise and another perfectly round one - the one with noise will represent the rocks and mountains on the planet - while the smooth sphere will be for the water surface.

Instead of using the
noise(..)
function - we'll use the fractal brownian motion noise function - which produces more natural looking patterns.


Visualization of the planet surface (2d) - draw a
Visualization of the planet surface (2d) - draw a 'blue' line to show the distance of 1.0 - shows the water surface.


fn fbm(pvec3<f32>) -> f32 {
    var 
total 0.0;
    var 
amplitude 0.5;
    var 
frequency 1.0;
    for (var 
05i++) {
        
total += noise(frequency) * amplitude;
        
frequency *= 1.5;
        
amplitude *= 0.5;
    }
    return 
total;
}

// Sdf scene 
fn sdfScenept:vec3<f32> ) -> f32 
{
    
let height:f32 sdfSphere(ptvec3<f32>(0.0,0,0), 0.7 fbm(pt*1.3)*0.65 fbm(pt*6.3)*0.1 );
    
let water:f32  sdfSphere(ptvec3<f32>(0.0,0,0), 1.0 );
    
let d sdfUnionheightwater );
    return 
d;
}

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let zoom 2.0;
    
let nuv = ( uv 2.0 1.0 ) * zoom;
 
    
let point vec3nuv 0.0 );
    
    
let d sdfScenepoint );

    var 
color vec3abs(sin(d*40.0)) );

    if ( 
abs(d) < 0.01 )
    {
        
color vec3<f32>(0.00.80.0);
    }  
    
// if inside - use red rings for the gradients
    
if ( 0.0 )
    {
         
color *= vec3<f32>(1.00.00.0);   
    }
    
    
// thin blue line representing the water surface
    
if ( abs(length(point)-1.0)<0.015 ) { color vec3<f32>(001); }
    
    return 
vec4<f32>(color1.0); 
}


Color (Height)


Visually display the height information using different colors - use the distance from the origin (i.e.,
length(point)
- as we'll assume the planet is at the origin).


Set height thresholds and link them to colors - blend between the color values - to create an asthetic visualization.
Set height thresholds and link them to colors - blend between the color values - to create an asthetic visualization.


fn colorGradient(valuef32) -> vec3<f32> {
    
// Define thresholds (adjust these as needed)
    
let h0 0.0;   // Start of blue
    
let h1 1.0;   // Start of blue-sand blend
    
let h2 1.02;   // Start of sand-green blend
    
let h3 1.1;   // Start of green-brown blend
    
let h4 1.15;   // Start of brown (pure color)
    
    // Define colors (in linear RGB space)
    
let blue   vec3<f32>(0.10.20.8);
    
let sand   vec3<f32>(0.760.70.5);
    
let green  vec3<f32>(0.20.60.3);
    
let brown  vec3<f32>(0.450.30.2);
    
let dark   vec3<f32>(0.10.10.1); // For values beyond h4
    
    // Calculate blended colors
    
if (value <= h0) {
        return 
blue;
    } else if (
value <= h1) {
        return 
blue;
    } else if (
value <= h2) {
        
let t smoothstep(0.01.0, (value h1) / (h2 h1));
        return 
mix(bluesandt);
    } else if (
value <= h3) {
        
let t smoothstep(0.01.0, (value h2) / (h3 h2));
        return 
mix(sandgreent);
    } else if (
value <= h4) {
        
let t smoothstep(0.01.0, (value h3) / (h4 h3));
        return 
mix(greenbrownt);
    }
    return 
brown;
}

// Sdf scene 
fn sdfScenept:vec3<f32> ) -> f32 
{
    
let height:f32 sdfSphere(ptvec3<f32>(0.0,0,0), 0.7 fbm(pt*1.0)*0.60 fbm(pt*6.3)*0.1 );
    
let water:f32  sdfSphere(ptvec3<f32>(0.0,0,0), 1.0 );
    
let d sdfUnionheightwater );
    return 
d;
}

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let zoom 2.0;
    
let nuv = ( uv 2.0 1.0 ) * zoom;
 
    
let point vec3nuv 0.0 );
    
    
let d sdfScenepoint );

    var 
color colorGradientlength(point) );
    
    
color color vec30.5 abs(sin(d*50.0))*0.5 );

    if ( 
abs(d) < 0.01 )
    {
        
color vec3<f32>(0.00.80.8);
    }  
    
// if inside - use red rings for the gradients
    
if ( > -0.01 && 0.0 )
    {
         
color vec3<f32>(1.0);   
    }
    
    
// thin blue line representing the water surface
    
if ( abs(length(point)-1.0)<0.005 ) { color vec3<f32>(00.50.8); }
    
    return 
vec4<f32>(color1.0); 
}


Cloud (Solid) Shell


We'll shift our attention from the planet to the clouds - we'll just do the clouds. The clouds will be a shell (or ring) that is above the surface of the planet.

To do this, we'll start by constructing a 'ring' (or shell in 3d).


Simple solid shell - specify the inner and outer distance.
Simple solid shell - specify the inner and outer distance.


fn sdfScenept:vec3<f32> ) -> f32 
{
    
let cloud_inner_radius 1.1;  // Start of cloud layer (soft)
    
let cloud_outer_radius 1.3;  // End of cloud layer (soft)
    
    // Cloud Shell
    
let inner sdfSphere(ptvec3<f32>(0.0), cloud_inner_radius);
    
let outer sdfSphere(ptvec3<f32>(0.0), cloud_outer_radius);
    
let shell sdfDifferenceouterinner );
    
    return 
shell;
}

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let zoom 2.0;
    
let nuv = ( uv 2.0 1.0 ) * zoom;
 
    
let point vec3nuv 0.0 );
    
    
let d sdfScenepoint );

    var 
color vec3(1.0); // colorGradient( length(point) );
    
    
color color vec3abs(sin(d*50.0)) );

    if ( 
0.0  )
    {
        
color vec3<f32>(0.80.00.0);
    }  
    
// if inside - use red rings for the gradients
    
if ( > -0.01 && 0.0 )
    {
         
color vec3<f32>(0.01.00.0);   
    }
    return 
vec4<f32>(color1.0); 
}


Clouds - Adding Noise Shell (Looks Fluffy)



We use a
softintersection(..)
sdf function to merge the sdfNoise with the cloud shell - to turn it from a solid ring to something that looks more like a cloud layer.


Mix noise with a thick shell to create a cloud like hemisphere.
Mix noise with a thick shell to create a cloud like hemisphere.


fn sdfScenept:vec3<f32> ) -> f32 
{
    
let cloud_inner_radius 1.1;  // Start of cloud layer (soft)
    
let cloud_outer_radius 1.3;  // End of cloud layer (soft)
    
    // Cloud Shell
    
let inner sdfSphere(ptvec3<f32>(0.0), cloud_inner_radius);
    
let outer sdfSphere(ptvec3<f32>(0.0), cloud_outer_radius);
    
let shell sdfDifferenceouterinner );
    
    
// Noise to shell
    
let n sdfNoisept*4.0 vec3(mytimer*vec3(0.1,0.05,0.1)), 0.25 );
    
let clouds smoothintersectionshelln0.7);
    
    return 
clouds;
}


Subtle Noise


If we want to add in some extra subtle 'noise' to make the clouds more furry on the surface - we can add a bit of noise to the surface with a high frequency component.


Add small abount of high frequency (fractal) noise to make the cloud surfaces more fluffy.
Add small abount of high frequency (fractal) noise to make the cloud surfaces more fluffy.


fn sdfScenept:vec3<f32> ) -> f32 
{
    
let cloud_inner_radius 1.1;  // Start of cloud layer (soft)
    
let cloud_outer_radius 1.3;  // End of cloud layer (soft)
    
    // Cloud Shell
    
let inner sdfSphere(ptvec3<f32>(0.0), cloud_inner_radius fbm(pt*10.0)*0.1 );
    
let outer sdfSphere(ptvec3<f32>(0.0), cloud_outer_radius fbm(pt*9.0)*0.1 );
    
let shell sdfDifferenceouterinner );
    
    
// Noise to shell
    
let n sdfNoisept*4.0 vec3(mytimer*vec3(0.1,0.05,0.1)), 0.25 );
    
let clouds smoothintersectionshelln0.7);
    
    return 
clouds;
}


Union Clouds and Planet


We can combine both the clouds and sky and draw a cross section of them both. We'll also add the cloud color to the gradient - so the very high values are greenishblue - it'll be white later on - it's just so we can see the solid clouds in the visualization - if we draw them white - they might get mixed up with the background.


Show the 2d cross section of the planet and cloud (with height lookup color gradient).
Show the 2d cross section of the planet and cloud (with height lookup color gradient).



fn colorGradient(valuef32) -> vec3<f32> {
    
// Define thresholds (adjust these as needed)
    
let h0 0.0;   // Start of blue
    
let h1 1.0;   // Start of blue-sand blend
    
let h2 1.02;   // Start of sand-green blend
    
let h3 1.1;   // Start of green-brown blend
    
let h4 1.15;   // Start of brown (pure color)
    
let h5 1.2;   // 
    
    // Define colors (in linear RGB space)
    
let blue   vec3<f32>(0.10.20.8);
    
let sand   vec3<f32>(0.760.70.5);
    
let green  vec3<f32>(0.20.60.3);
    
let brown  vec3<f32>(0.450.30.2);
    
let dark   vec3<f32>(0.10.10.1);
    
let white  vec3<f32>(0.01.01.0);
    
    
// Calculate blended colors
    
if (value <= h0) {
        return 
blue;
    } else if (
value <= h1) {
        return 
blue;
    } else if (
value <= h2) {
        
let t smoothstep(0.01.0, (value h1) / (h2 h1));
        return 
mix(bluesandt);
    } else if (
value <= h3) {
        
let t smoothstep(0.01.0, (value h2) / (h3 h2));
        return 
mix(sandgreent);
    } else if (
value <= h4) {
        
let t smoothstep(0.01.0, (value h3) / (h4 h3));
        return 
mix(greenbrownt);
    } else if (
value <= h5) {
        
let t smoothstep(0.01.0, (value h4) / (h5 h4));
        return 
mix(brownwhitet);
    }
    return 
white;
}

fn 
sdfScenept:vec3<f32> ) -> f32 
{
    
// Planet
    
let planet_radius 1.0;
    
let water  sdfSphere(ptvec3<f32>(0.0), planet_radius );
    
let height sdfSphere(ptvec3<f32>(0.0), planet_radius*0.75 fbm(pt*1.332)*0.6);
    var 
planet sdfUnionwaterheight );
    
    
// Clouds
    
let cloud_inner_radius 1.1;  // Start of cloud layer (soft)
    
let cloud_outer_radius 1.3;  // End of cloud layer (soft)
    
    // Cloud Shell
    
let inner sdfSphere(ptvec3<f32>(0.0), cloud_inner_radius fbm(pt*6.0)*0.1 );
    
let outer sdfSphere(ptvec3<f32>(0.0), cloud_outer_radius fbm(pt*5.0)*0.1 );
    
let shell sdfDifferenceouterinner );
    
    
// Noise to cloud shell
    
let n sdfNoisept*4.0 vec3(mytimer*vec3(0.1,0.05,0.1)), 0.25 );
    
let clouds smoothintersectionshelln0.7);
    
 
    
// Combine
    
return sdfUnion(planetclouds); // Show planet and clouds
}

@
fragment
fn main(@location(0uv vec2<f32>) -> @location(0vec4<f32
{
    
let zoom 2.0;
    
let nuv = ( uv 2.0 1.0 ) * zoom;
 
    
let point vec3nuv 0.0 );
    
    
let d sdfScenepoint );

    var 
color vec3(1.0); // colorGradient( length(point) );
    
    
color color vec3abs(sin(d*50.0)) );

    if ( 
0.0  )
    {
        
color colorGradientlength(point) );
    }  
    
// if inside - use red rings for the gradients
    
if ( > -0.01 && 0.0 )
    {
         
color vec3<f32>(0.01.00.0);   
    }
    return 
vec4<f32>(color1.0); 
}


An important thing to remember is the clouds are opaque - so when we come to draw the scene in 3-dimensions - we're going to have to ray-march through the cloud (keeping track of the distance for the density/transparency calculation).

Ray-Tracing (3D)


We'll pass a variable to the
sdfScene(..)
so we can draw the planet and the clouds seperately - this will be useful later on when we want to march through the clouds. As we'll detect a cloud intersection - then we'll step through using a fixed distance to work out the density and the transparency.

Next, we'll change the upper color to white - in the previous sectoin - we used a green-bluish so we could see it in the 2d cross sectional view - but we'll use white for the upper mountain tops and clouds.

We'll also mixin a background - it makes the scene look nicer - we basically use the uv coordinates to create a background gradient.

We also add in a rotation (using a simple timer) to rotate the clouds and planet - so they're spinning- this gives us a better view of the result - so we can see how the planet looks from different angles.

No lighting calculations yet - we're just using the raw color - so it doesn't look that sexy - but it will in a while - once you add lighting and shows you'll be drooling.


The ray-traced cloud and planet (with no lighting).
The ray-traced cloud and planet (with no lighting).


fn sdfScenept:vec3<f32>, doPlanet:bool ) -> f32 
{
    
// Planet
    
let planet_radius 1.0;
    
let water  sdfSphere(ptvec3<f32>(0.0), planet_radius );
    
let height sdfSphere(ptvec3<f32>(0.0), planet_radius*0.75 fbm(pt*1.332)*0.6);
    var 
planet sdfUnionwaterheight );
    
    
// Clouds
    
let cloud_inner_radius 1.1;  // Start of cloud layer (soft)
    
let cloud_outer_radius 1.3;  // End of cloud layer (soft)
    
    // Cloud Shell
    
let inner sdfSphere(ptvec3<f32>(0.0), cloud_inner_radius fbm(pt*6.0)*0.1 );
    
let outer sdfSphere(ptvec3<f32>(0.0), cloud_outer_radius fbm(pt*5.0)*0.1 );
    
let shell sdfDifferenceouterinner );
    
    
// Noise to cloud shell
    
let n sdfNoisept*4.0 vec3(mytimer*vec3(0.1,0.05,0.1)), 0.35 );
    
let clouds smoothintersectionshelln0.7);
    
 
    if ( 
doPlanet )
    {
        return 
planet;
    }
    return 
clouds;
}

fn 
rayCastposition:vec3<f32>, planet:bool ) -> f32
{
    var 
t:f32 0.0;

    
// Rotation 
    
var mytimer*0.1;
    var 
mytimer*0.01;
    var 
Rx:mat4x4<f32> = mat4x4<f32>( vec4<f32>( cos(x),-sin(x), 0,      0),
                                      
vec4<f32>( sin(x), cos(x), 0,      0),
                                      
vec4<f32>( 0,      0,      1,      0),
                                      
vec4<f32>( 0,      0,      0,      1));
    var 
Ry:mat4x4<f32> = mat4x4<f32>( vec4<f32>( cos(y), 0,      sin(y), 0),
                                      
vec4<f32>( 0,      1,      0,      0),
                                      
vec4<f32>(-sin(y), 0,      cos(y), 0),
                                      
vec4<f32>( 0,      0,      0,      1));
 
     var 
pt = (vec4<f32>(position1) * (Ry*Rx) ).xyz;
    
    
sdfSceneptplanet );

    return 
t;
}

fn 
rayTracerayPos:vec3<f32>,  rayDir:vec3<f32>, planet:bool ) ->f32
{
    var 
0.0;
    var 
numSteps:i32 128;
    var 
shrinkSize:f32 1.0;
    var 
ds 0.9;
        
    for(var 
i:i32 0numStepsi++)
    {
        
ds *= shrinkSize;
            
        var 
samplePoint:vec3<f32> = rayPos rayDir*t;
        var 
dist rayCast(samplePointplanet) * ds;

        
+= dist;
        if ( 
0.001 || 50 )
        {
            return 
0.0// no hit
        
}
    }
    return 
t;
}

fn 
background(eyeDirectionvec3<f32>) -> vec3<f32> {
    
let sun_color vec3<f32>(1.00.90.55);
    
let sun_amount dot(eyeDirectionvec3<f32>(0.00.01.0));

    var 
sky mix(
        
vec3<f32>(0.00.050.2),
        
vec3<f32>(0.150.30.4),
        
1.0 eyeDirection.y
    
);
    
    
sky += sun_color min(pow(sun_amount30.0) * 5.01.0);
    
sky += sun_color min(pow(sun_amount10.0) * 0.61.0);

    return 
sky;
}

@
fragment
fn main(@location(0coords vec2<f32>) -> @location(0vec4<f32
{
       var 
uv    = (-1.0 2.0*coords.xy);

    var 
rayPos:vec3<f32> = vec3<f32>(00.15.0);
    var 
rayDir:vec3<f32> = normalizevec3<f32>( uv*1.50.0 ) - rayPos ); 

    var 
fragColor:vec4<f32>  = vec4backgroundvec3<f32>(uv0.0) ), 1.0 );
    
    
// trace the planet
    
var res rayTrace(rayPosrayDirtrue);
    if ( 
res 0.0 )
    {
        
let hitPos rayPos rayDir res;
        var 
color colorGradientlength(hitPos) );
        
fragColor vec4<f32>( color1.0 );
    }
    
    
// ray-trace the clouds
    
res rayTrace(rayPosrayDirfalse);
    if ( 
res 0.0 )
    {
        
let hitPos rayPos rayDir res;
        var 
color colorGradientlength(hitPos) );
        
fragColor vec4<f32>( color1.0 );
    }
    
    return 
fragColor;
}


Normals and Lighting


We calculate the normal for the intersection point on the SDF surface - which can be used to for a simple lighting calculation (i.e., dot product with the light direction and the normal).

fn rayNormal(posvec3<f32>, planet:bool) -> vec3<f32> {
    
let eps 0.001;
    
let d rayCast(posplanet);
    return 
normalize(vec3<f32>(
        
rayCast(pos vec3<f32>(eps0.00.0), planet) - d,
        
rayCast(pos vec3<f32>(0.0eps0.0), planet) - d,
        
rayCast(pos vec3<f32>(0.00.0eps), planet) - d
    
));
}


Add this into the main draw code - to enhance the final color (so it isn't flat and lifeless).


Adding a bit of lighting.
Adding a bit of lighting.


@fragment
fn main(@location(0coords vec2<f32>) -> @location(0vec4<f32
{
       var 
uv    = (-1.0 2.0*coords.xy);

    var 
rayPos:vec3<f32> = vec3<f32>(00.15.0);
    var 
rayDir:vec3<f32> = normalizevec3<f32>( uv*1.50.0 ) - rayPos ); 

    var 
fragColor:vec4<f32>  = vec4backgroundvec3<f32>(uv0.0) ), 1.0 );
    
    
// trace the planet
    
var res rayTrace(rayPosrayDirtrue);
    if ( 
res 0.0 )
    {
        
let hitPos rayPos rayDir res;
        var 
color colorGradientlength(hitPos) );
        
        var 
lightDir = -rayDir;// ray direction as light direction
        
var normal   rayNormalhitPostrue );
        
color    = (color clamp(dotlightDirnormal ), 0.01.0) *0.8);
        
        
fragColor vec4<f32>( color1.0 );
    }
    
    
    
// ray-trace the clouds
    
res rayTrace(rayPosrayDirfalse);
    if ( 
res 0.0 )
    {
        
let hitPos rayPos rayDir res;
        var 
color colorGradientlength(hitPos) );
        
        var 
lightDir = -rayDir;// ray direction as light direction
        
var normal   rayNormalhitPosfalse );
        
color    = (color abs(dotlightDirnormal )) *0.8);
        
        
fragColor vec4<f32>( color1.0 );
    }
    
    return 
fragColor;
}


The clouds are 'solid' - and the planet looks good - but we can take it further - we want to tweak the parameters so the planet surface looks a little nice (more water and a bit more bumpy). We also want to adjust the thickness and density of the clouds.

But most important - instead of treating the clouds as a solid object - we want to add 'transparency' - so we'll add some ray-marching in to calculate the density (distance) of a ray travelling through a cloud.


Ray-Marching (Cloud Transparency/Density) and Shadows


With a few extra lines - we'll modify the main function - so it marches through the cloud- instead of just hitting the surface and calculating the final color. We'll keep track of which points are inside the cloud as it marches through the cloud - this will be used as an approximation for the density - if the density is too high - the cloud is very thick and we can't see through it - otherwise if it's not very thick - we can see the planet through the cloud.


Ray-marching and shadows to make the planet with clouds more asthetically pleasing and realistic.
Ray-marching and shadows to make the planet with clouds more asthetically pleasing and realistic.


@fragment
fn main(@location(0coords vec2<f32>) -> @location(0vec4<f32
{
       
let uv    = (-1.0 2.0*coords.xy);

    var 
rayPos:vec3<f32> = vec3<f32>(00.15.0);
    var 
rayDir:vec3<f32> = normalizevec3<f32>( uv*1.50.0 ) - rayPos ); 

    
let lightDir normalizevec3<f32>(0.20.2,0.9) );
    var 
fragColor:vec4<f32>  = vec4backgroundvec3<f32>(uv0.0) ), 1.0 );
    
    
// trace the planet
    
var res rayTrace(rayPosrayDirtrue);
    if ( 
res 0.0 )
    {
        
let hitPos rayPos rayDir res;
        var 
color colorGradientlength(hitPos) );

        var 
normal   rayNormalhitPostrue );
        
color    = (color clamp(dotlightDirnormal ), 0.01.0) *0.8);
        
        
fragColor vec4<f32>( color1.0 );
        
        
// Add shadow - cloud is shown on the surface of teh 
        
var resShadow rayTrace(hitPos+normal*0.01, -lightDirfalse);
        if ( 
resShadow 0.0 )
        {
            
fragColor *= 0.8;
        }
    }
    
    
// ray-trace clouds - overlay the opaque clouds (transparency)
    // march 'through' the ray - taking into account the 'density'
    
res rayTrace(rayPosrayDirfalse);
    if ( 
res 0.0 )
    {
        
// march through the cloud till we are outside of the cloud (approx)
        
var density:f32 0.0;
        var 
rayPos1 rayPos rayDir*res;
        for (var 
gg:i32=0gg<16gg++)
        {
            
rayPos1 += rayDir*0.03;
            
let d rayCast(rayPos1false);
            if ( 
0.0 // inside the cloude
            
{
                
density += 0.05;
            }
            
            
// hit planet or reached maximum distance
            
let p rayCast(rayPos1true);
            if ( 
0.0 )
            {
                break;
            }
        }
        
        
let hitPos rayPos rayDir*res;
        
// hit a cloud
        
var color    vec4<f32>(1.0);
        var 
normal   rayNormalhitPosfalse );
        
fragColor    += color * ( 0.2 clamp(absdotlightDirnormal )), 0.01.0) ) * density;

    }
    
    return 
fragColor;
}


Subtle bug in the implementation above which you might not notice - however, it's also drawing clouds 'behind' the planet - so you need to add an extra check to only draw clouds in front of the planet that the camera can see. This is the required fix:

    // get distance to planet intersection check it's greater than the cloud distance
    
var resPlanet rayTrace(rayPosrayDirtrue);
    if ( 
resPlanet <= 0.0 ) { resPlanet 100.0; }
    
    
res rayTrace(rayPosrayDirfalse);
    if ( 
res 0.0 && res<resPlanet )
    {
    ....


Tinker with Noise Parameters


Making some modifications to the noise parameters we can make our little planet look better.


Tweak some of the noise parameters to make things look nicer.
Tweak some of the noise parameters to make things look nicer.


fn sdfScenept:vec3<f32>, doPlanet:bool ) -> f32 
{
    
// Planet
    
let planet_radius 1.0;
    
let water  sdfSphere(ptvec3<f32>(0.0), planet_radius );
    
let height sdfSphere(ptvec3<f32>(0.0), planet_radius*0.75 fbm(pt*1.532)*0.35 fbm(pt*5.332)*0.1 fbm(pt*11.332)*0.05 );
    var 
planet sdfUnionwaterheight );
    
    
// Clouds
    
let cloud_inner_radius 1.0;  // Start of cloud layer (soft)
    
let cloud_outer_radius 1.4;  // End of cloud layer (soft)
    
    // Cloud Shell
    
let inner sdfSphere(ptvec3<f32>(0.0), cloud_inner_radius fbm(pt*6.0)*0.0 );
    
let outer sdfSphere(ptvec3<f32>(0.0), cloud_outer_radius fbm(pt*5.0)*0.0 );
    
let shell sdfDifferenceouterinner );
    
    
// Noise to cloud shell
    
let n sdfNoisept*4.0 vec3(mytimer*vec3(0.1,0.05,0.1)), 0.46 );
    
let clouds smoothintersectionshelln0.7);
    
 
    if ( 
doPlanet )
    {
        return 
planet;
    }
    return 
clouds;
}


This is only the beginning - as you can create all sorts of worlds - for example, disable the water height, remove clouds and use a grayscale color values for the height-color information and you've got an asteroid.


Instead of a planet - we can create little astroid - we also swap the background gradient to stars so it looks mor correct.
Instead of a planet - we can create little astroid - we also swap the background gradient to stars so it looks mor correct.


For th asteroid - instead of a cloud floating around the surface - maybe you could have small rocks? Or dust eminating from the ground surface? All sorts of things you could try out with this. You could even gamify the concept - build a mini asteroids game - but the asteroids, planet and ships etc. are all built procedurally using sdf functions. Cool eh?


Things to Try


Lots of other ways you can take the idea - extend the current implementation or add more features, such as:

• add moon to the planet (orbits)
• small satelites
• little boats
• sun? (lens flare)
• solar system (complete collection of planets)
• detailed planet surface analysis (e.g., mimic moon/earth greater detail)
• other atmospheres


Resouces & Links


2d SDF Noise Shell (Sphere/Cloud)

v1 Planet and Cloud (3d/lighting) Full Code/Demo WebGPU Lab

v2 Planet and Cloud (3d/lighting) Full Code/Demo WebGPU Lab

Water Depth/Color Fixes - Full Code/Demo WebGPU Lab

Fractal Brownian Motion Noise (Interactive Visualization)

Little Asteroid (Remove Clouds and Water - Gray)

Add more height colors/background gradient and tinker with the noise parameters











Ray-Tracing with WebGPU kenwright WebGPU Development Cookbook - coding recipes for all your webgpu needs! WebGPU by Example: Fractals, Image Effects, Ray-Tracing, Procedural Geometry, 2D/3D, Particles, Simulations WebGPU Games WGSL 2d 3d interactive web-based fun learning WebGPU Compute WebGPU API - Owners WebGPU & WGSL Essentials: A Hands-On Approach to Interactive Graphics, Games, 2D Interfaces, 3D Meshes, Animation, Security and Production Kenwright graphics and animations using the webgpu api 12 week course kenwright learn webgpu api kenwright programming compute and graphics applications with html5 and webgpu api kenwright real-time 3d graphics with webgpu kenwright webgpu for dummies kenwright webgpu api develompent a quick start guide kenwright webgpu by example 2022 kenwright webgpu gems kenwright webgpu interactive compute and graphics visualization cookbook kenwright wgsl webgpu shading language cookbook kenwright WebGPU Shader Language Development: Vertex, Fragment, Compute Shaders for Programmers Kenwright wgsl webgpugems shading language cookbook kenwright WGSL Fundamentals book kenwright WebGPU Data Visualization Cookbook kenwright Special Effects Programming with WebGPU kenwright WebGPU Programming Guide: Interactive Graphics and Compute Programming with WebGPU & WGSL kenwright



 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2025 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.