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;
}
};
