No 3D Model Needed! Procedural Asteroid in Nuke 17

Generating detailed asteroids or rocky debris usually means sculpting assets, projecting textures, or relying on 3D renders. For many comp tasks this is far more than what is needed. A lot of shots only require a single turntable element, a drifting foreground rock, or a stylized asteroid field. With Nuke 17 and BlinkScript, you can now create these assets procedurally inside the comp.

The SDF_Asteroid BlinkScript is a lightweight raymarcher that builds a lumpy, organic asteroid using nothing more than a sphere, layered noise, and shading code. It requires no model, no UVs, and no textures. Everything is generated mathematically in real time.


Why an Asteroid in BlinkScript

Compositors often need flexible, easily tweakable hero elements that can be adjusted after client notes. Procedural tools shine here. Instead of waiting for a 3D re-render, the entire look of the asteroid is controlled through parameters that can be animated or versioned instantly.

Raymarching an SDF (signed distance field) gives several advantages:

  • Clean silhouettes without polygon edges
  • Infinite detail depending on noise layers
  • Stable shading because normals are derived analytically
  • No UV seams and no texture management
  • Procedural albedo variation built directly into the node

This approach can replace many common space rock elements used in matte paintings, motion graphics, and sci fi environments.


How the Shape Is Created

At the core of the script is a simple idea:

Start with a sphere and displace its surface with fractal noise.

Base Sphere

The radius parameter defines the underlying shape. Everything else is a displacement on top of it.

Ridged fBm Noise

The displacement uses ridged fBm which creates sharp creases, plates, and mountain like ridges that feel natural for rocky bodies. Unlike smooth Perlin style bumps, ridged noise gives:

  • Cracked surfaces
  • Hard discontinuities
  • Broken strata

This is ideal for asteroids.

Domain Warp

To avoid a perfectly spherical silhouette, the script warps the noise domain using a second noise pass. Domain warp introduces large scale asymmetry which produces:

  • Bulges and cavities
  • Uneven silhouette outlines
  • Variation across major forms

The combination yields a believable asteroid with both macro and micro detail.


Surface Texture and Albedo

The node does not rely on external textures. Instead, it generates a procedural albedo based on a tri planar noise approach. Three noise samples in X, Y, and Z directions are blended to produce streaks and layers.

The result gives a sense of geological strata without UV coordinates. rockColor acts as the base material, and the noise darkens or lightens different regions organically.


Lighting, AO, and Shadows

The shading system inside the kernel includes several features normally associated with 3D engines.

Diffuse and Specular

A user controlled light direction drives the main shading. roughness and specGain allow you to define:

  • Chalky diffuse rocks
  • Harder reflective meteorites

Ambient Occlusion

The script includes a simple ambient occlusion estimator that samples along the surface normal. This creates depth inside cracks and cavities, making the asteroid feel grounded and physically present.

Hard Shadows

A small raymarch based shadow test adds a crisp falloff on the surface. shadowHard controls how quickly shadows deepen.

Rim Lighting

A subtle fresnel term boosts the silhouette in backlit situations which helps define form in darker scenes.


Camera and Animation Controls

The script has its own camera system with FOV, position, target, and up vector. This makes the asteroid entirely self contained. For quick turntables, animate:

  • Time
  • Rot_YPR

Both produce smooth spins without building any 3D rig in Nuke.

You can also scale, offset, or reposition the asteroid using modelScale and modelPos.


Performance Notes

Even though it is a raymarcher, the script is fast enough for interactive previews thanks to step clamping and distance guided marching. To speed up iteration:

  • Reduce displacement amplitude for previews
  • Lower octaves
  • Use smaller resolution while you tune the look

For final frames, push octaves to four or six for high detail.


When This Tool Is Useful

  • Asteroid fields for sci fi scenes
  • Background rocks for space station shots
  • Motion graphics elements
  • Destruction debris
  • Floating stylized geometry
  • Quick prototypes for previs and pitch frames

Because it is procedural, you can generate dozens of unique asteroids simply by changing the seed value.


Final Thoughts

The SDF_Asteroid BlinkScript is a great example of how far you can push Nuke’s GPU kernel system. It handles modeling, displacement, shading, AO, and rendering inside a single node. For compositors who want fast turnaround and total creative control, this is a powerful way to build 3D looking assets without leaving the comp.

Blink script, Please note this works only Nuke 16.2 and above

// Asteroid_Mini3D : simple noise-displaced sphere (asteroid) with lighting/AO/shadow
// Output: RGBA. Paste into a BlinkScript node.

kernel Asteroid_Mini3D : ImageComputationKernel<ePixelWise>
{
  Image<eWrite, eAccessPoint> dst;

param:
  // Render / camera
  float2 res;        // set to format size
  float  fov;        // vertical FOV (deg)
  float3 camPos;
  float3 camTarget;
  float3 camUp;

  // Object transform
  float3 modelPos;
  float3 rotYPR;     // yaw, pitch, roll (deg)
  float  modelScale;

  // Base asteroid shape
  float  radius;     // base sphere radius

  // Displacement noise (value noise fBm)
  int    seed;
  int    octaves;    // 1..6
  float  baseFreq;   // base frequency
  float  gain;       // amplitude falloff per octave
  float  lacunarity; // frequency multiplier per octave
  float  amp;        // displacement amplitude
  float  warpAmp;    // domain warp amplitude
  float  warpFreq;   // domain warp frequency

  // Shading
  float3 rockColor;
  float3 lightDir;
  float3 lightCol;
  float3 ambientCol;
  float  roughness;  // lower = sharper spec
  float  specGain;
  float  aoGain;
  float  shadowHard;

  // Output
  float  maxDepth;   // march limit
  float  time;       // seconds (for animation if desired)

  void define(){
    defineParam(res,         "Resolution",  float2(1280.0f, 720.0f));
    defineParam(fov,         "FOV",         40.0f);
    defineParam(camPos,      "CamPos",      float3(0.0f, 0.0f, 4.0f));
    defineParam(camTarget,   "CamTarget",   float3(0.0f, 0.0f, 0.0f));
    defineParam(camUp,       "CamUp",       float3(0.0f, 1.0f, 0.0f));

    defineParam(modelPos,    "ModelPos",    float3(0.0f, 0.0f, 0.0f));
    defineParam(rotYPR,      "Rot_YPR",     float3(25.0f, -10.0f, 0.0f));
    defineParam(modelScale,  "ModelScale",  1.0f);

    defineParam(radius,      "Radius",      0.95f);

    defineParam(seed,        "Seed",        1337);
    defineParam(octaves,     "Octaves",     4);
    defineParam(baseFreq,    "BaseFreq",    1.2f);
    defineParam(gain,        "Gain",        0.55f);
    defineParam(lacunarity,  "Lacunarity",  2.0f);
    defineParam(amp,         "DisplaceAmp", 0.28f);
    defineParam(warpAmp,     "WarpAmp",     0.18f);
    defineParam(warpFreq,    "WarpFreq",    0.9f);

    defineParam(rockColor,   "RockColor",   float3(0.32f, 0.31f, 0.30f));
    defineParam(lightDir,    "LightDir",    float3(0.6f, 0.7f, 0.4f));
    defineParam(lightCol,    "LightColor",  float3(1.0f, 0.98f, 0.95f));
    defineParam(ambientCol,  "Ambient",     float3(0.07f, 0.07f, 0.075f));
    defineParam(roughness,   "Roughness",   0.38f);
    defineParam(specGain,    "SpecGain",    0.22f);
    defineParam(aoGain,      "AOGain",      0.65f);
    defineParam(shadowHard,  "ShadowHard",  1.0f);

    defineParam(maxDepth,    "MaxDepth",    8.0f);
    defineParam(time,        "Time",        0.0f);
  }

  // -------- helpers (kept simple) --------
  float3 normalizeSafe(float3 v){ float L=length(v); return (L>0.0f)? v/L : float3(0.0f); }
  float3 rotateX(float3 p,float a){ float c=cos(a),s=sin(a); return float3(p.x, c*p.y - s*p.z, s*p.y + c*p.z); }
  float3 rotateY(float3 p,float a){ float c=cos(a),s=sin(a); return float3(c*p.x + s*p.z, p.y, -s*p.x + c*p.z); }
  float3 rotateZ(float3 p,float a){ float c=cos(a),s=sin(a); return float3(c*p.x - s*p.y, s*p.x + c*p.y, p.z); }

  float hashf(int s){
    s = (s<<13) ^ s;
    int m = (s*(s*s*15731 + 789221) + 1376312589);
    return 1.0f - (float)(m & 0x7fffffff) / 1073741824.0f;
  }

  // 3D value-noise (0..1), trilinear interp + cubic fade
  float vnoise3(float3 p, int s){
    float3 ip = floor(p);
    float3 f  = p - ip;
    float3 u  = f*f*(float3(3.0f,3.0f,3.0f) - 2.0f*f);

    int ix=(int)ip.x, iy=(int)ip.y, iz=(int)ip.z;
    int K = 1103515245;
    float n000 = hashf(s + (ix  )*K + (iy  )*97 + (iz  )*307);
    float n100 = hashf(s + (ix+1)*K + (iy  )*97 + (iz  )*307);
    float n010 = hashf(s + (ix  )*K + (iy+1)*97 + (iz  )*307);
    float n110 = hashf(s + (ix+1)*K + (iy+1)*97 + (iz  )*307);
    float n001 = hashf(s + (ix  )*K + (iy  )*97 + (iz+1)*307);
    float n101 = hashf(s + (ix+1)*K + (iy  )*97 + (iz+1)*307);
    float n011 = hashf(s + (ix  )*K + (iy+1)*97 + (iz+1)*307);
    float n111 = hashf(s + (ix+1)*K + (iy+1)*97 + (iz+1)*307);

    float nx00 = n000*(1.0f-u.x) + n100*u.x;
    float nx10 = n010*(1.0f-u.x) + n110*u.x;
    float nx01 = n001*(1.0f-u.x) + n101*u.x;
    float nx11 = n011*(1.0f-u.x) + n111*u.x;
    float nxy0 = nx00*(1.0f-u.y) + nx10*u.y;
    float nxy1 = nx01*(1.0f-u.y) + nx11*u.y;
    return nxy0*(1.0f-u.z) + nxy1*u.z; // 0..1
  }

  // fBm sum centered to ~-1..1 then back to 0..1
  float fbm3(float3 p, int s, int octs, float f0, float g, float lac){
    float a=1.0f, f=f0, sum=0.0f, norm=0.0f;
    for(int i=0;i<8;i++){
      if(i>=octs) break;
      float n = vnoise3(p*f, s + i*97)*2.0f - 1.0f;
      sum  += n * a;
      norm += a;
      a *= g;
      f *= lac;
    }
    if(norm>0.0f) sum/=norm;
    return sum*0.5f + 0.5f; // 0..1
  }

  float3 warp3(float3 p, int s, float wf, float wa){
    float3 w = float3(
      vnoise3(p*wf + float3(31.1f, 7.9f,  5.3f), s+11),
      vnoise3(p*wf + float3(11.7f, 23.4f, 3.1f), s+23),
      vnoise3(p*wf + float3(5.9f,  3.7f,  29.5f), s+31)
    );
    return p + (w - float3(0.5f))*2.0f*wa;
  }

  // ---- SDF & shading helpers ----
  float mapRock(float3 p){
    float3 pw = warp3(p, seed, warpFreq, warpAmp);
    float disp = (fbm3(pw, seed, octaves, baseFreq, gain, lacunarity) - 0.5f)*2.0f; // -1..1
    return length(p) - (radius + disp*amp);
  }

  float3 calcNormal(float3 p){
    float e=0.0018f;
    float3 ex=float3(e,0,0), ey=float3(0,e,0), ez=float3(0,0,e);
    float nx = mapRock(p+ex) - mapRock(p-ex);
    float ny = mapRock(p+ey) - mapRock(p-ey);
    float nz = mapRock(p+ez) - mapRock(p-ez);
    return normalizeSafe(float3(nx,ny,nz));
  }

  float ambientOcclusion(float3 p, float3 n){
    float occ=0.0f, sca=1.0f;
    for(int i=1;i<=5;i++){
      float hr=0.04f*(float)i;
      float dd=mapRock(p + n*hr);
      occ += (hr - dd)*sca;
      sca *= 0.6f;
    }
    float ao = 1.0f - aoGain*occ;
    if(ao<0.0f) ao=0.0f; if(ao>1.0f) ao=1.0f;
    return ao;
  }

  float hardShadow(float3 ro, float3 rd){
    float t=0.02f;
    for(int i=0;i<72;i++){
      float3 pos = ro + rd*t;
      float h = mapRock(pos);
      if(h < 0.0012f) return 0.0f;
      t += max(0.01f, h*0.9f) * shadowHard;
      if(t > 8.0f) break;
    }
    return 1.0f;
  }

  void process(int2 ip)
  {
    // camera ray
    float2 uv = float2((float)ip.x, (float)ip.y);
    float2 ndc = float2((uv.x/res.x - 0.5f)*(res.x/res.y), (0.5f - uv.y/res.y));
    float vfov = fov * 0.01745329252f;

    float3 fwd = normalizeSafe(camTarget - camPos);
    float3 right = normalizeSafe(cross(fwd, camUp));
    float3 upv = normalizeSafe(cross(right, fwd));
    float3 rd = normalizeSafe(fwd + right*ndc.x*tan(vfov*0.5f)*2.0f + upv*ndc.y*tan(vfov*0.5f)*2.0f);
    float3 ro = camPos;

    // object-space transform (inverse on ray)
    float yaw=rotYPR.x*0.01745329252f, pitch=rotYPR.y*0.01745329252f, roll=rotYPR.z*0.01745329252f;
    ro -= modelPos;
    ro = rotateY(rotateX(rotateZ(ro, -roll), -pitch), -yaw) / modelScale;
    rd = rotateY(rotateX(rotateZ(rd, -roll), -pitch), -yaw);

    // raymarch
    float t=0.0f; bool hit=false; float d=0.0f;
    for(int i=0;i<160;i++){
      float3 pos = ro + rd*t;
      d = mapRock(pos);
      if(d < 0.0012f){ hit=true; break; }
      t += clamp(d, 0.002f, 0.08f);
      if(t > maxDepth) break;
    }

    float3 col=float3(0.0f); float alpha=0.0f;

    if(hit){
      float3 pos = ro + rd*t;
      float3 nrm = calcNormal(pos);

      // world-space normal for lighting direction
      float3 wsN = rotateZ(rotateX(rotateY(nrm*modelScale, yaw), pitch), roll);
      wsN = normalizeSafe(wsN);

      float3 L = normalizeSafe(lightDir);
      float3 V = normalizeSafe(-rd);

      float ndotl = clamp(dot(wsN, L), 0.0f, 1.0f);
      float3 H = normalizeSafe(L + V);
      float ndoth = clamp(dot(wsN, H), 0.0f, 1.0f);
      float specExp = max(2.0f, 1.0f / max(0.02f, roughness));
      float spec = pow(ndoth, specExp) * specGain;

      float ao = ambientOcclusion(pos, wsN);
      float sh = hardShadow(pos + wsN*0.003f, L);

      // simple albedo modulation for variation
      float layers = (vnoise3(pos*1.3f, seed+101) + vnoise3(pos*2.1f, seed+109))*0.5f; // 0..1
      float3 albedo = rockColor * (0.65f + 0.35f*layers);

      col = ambientCol*ao + albedo*(ndotl*sh)*lightCol + spec*lightCol;

      // gentle fresnel to help the rim
      float fres = pow(1.0f - clamp(dot(wsN, V), 0.0f, 1.0f), 5.0f);
      col += albedo * (0.06f * fres);

      alpha = 1.0f;
    }

    SampleType(dst) o(0.0f);
    o[0]=col.x; o[1]=col.y; o[2]=col.z; o[3]=alpha;
    dst() = o;
  }
};

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x