// Minimal Mandelbulb fractal shader - test version
#define MARCH_MAX_STEPS 200
#define MARCH_MAX_DISTANCE 500.0
#define DE_MAX_ITER 25
#define DE_BAILOUT 10.0
#define HIT_STATUS_HIT 1
#define HIT_STATUS_MISS_DISTANCE 2
#define HIT_STATUS_MISS_STEPS 3
struct MarchResult
{
float distance;
int steps;
int hitStatus;
int totalDEIterations;
};
struct FractalFunctions
{
// Computes the world-space radius that a sphere must have to appear 1 pixel tall on screen at a given distance.
static float GetPixelWorldRadius(float distance)
{
return distance * ResolvedView.ClipToView._22 / ResolvedView.ViewSizeAndInvSize.y;
}
// Mandelbulb distance estimator
static float MandelbulbDE(float3 pos, float power, out int iterations)
{
float3 z = pos;
float dr = 1.0;
float r = 0.0;
for (iterations = 0; iterations < DE_MAX_ITER; iterations++)
{
r = length(z);
if (r > DE_BAILOUT)
break;
// Convert to spherical coordinates
float theta = acos(z.z / r);
float phi = atan2(z.y, z.x);
dr = pow(r, power - 1.0) * power * dr + 1.0;
// Scale and rotate the point
float zr = pow(r, power);
theta = theta * power;
phi = phi * power;
// Convert back to Cartesian coordinates
float sinTheta = sin(theta);
z = zr * float3(sinTheta * cos(phi), sinTheta * sin(phi), cos(theta));
z += pos;
}
return 0.5 * log(r) * r / dr;
}
// Ray marching
static MarchResult MarchFractal(float3 rayOrigin, float3 rayDir, float power)
{
float totalDist = 0.0;
int steps = 0;
int totalDEIterations = 0;
while (totalDist < MARCH_MAX_DISTANCE && steps < MARCH_MAX_STEPS)
{
steps++;
float3 pos = rayOrigin + rayDir * totalDist;
int deIterations;
float distance = MandelbulbDE(pos, power, deIterations);
totalDEIterations += deIterations;
float pixelSize = GetPixelWorldRadius(totalDist);
if (distance <= pixelSize)
{
MarchResult result;
result.distance = totalDist;
result.steps = steps;
result.hitStatus = HIT_STATUS_HIT;
result.totalDEIterations = totalDEIterations;
return result;
}
float stepSize = max(distance, pixelSize * 0.5);
totalDist += stepSize;
}
MarchResult result;
result.distance = totalDist;
result.steps = steps;
result.hitStatus = (totalDist >= MARCH_MAX_DISTANCE) ? HIT_STATUS_MISS_DISTANCE : HIT_STATUS_MISS_STEPS;
result.totalDEIterations = totalDEIterations;
return result;
}
static float3 RenderFractal(float2 screenUV, float3 sceneColor, float power)
{
// Get camera parameters
float3 cameraOrigin = LWCHackToFloat(ResolvedView.WorldCameraOrigin);
// Convert screen UV to clip space
float2 clipXY = screenUV * 2.0 - 1.0;
clipXY.y = -clipXY.y;
float4 clipPos = float4(clipXY, 1.0, 1.0);
float4 viewPos = mul(clipPos, ResolvedView.ClipToView);
viewPos.xyz /= viewPos.w;
// Transform view direction to world space
float3 worldDir = normalize(mul(viewPos.xyz, (float3x3)ResolvedView.ViewToTranslatedWorld));
// Scale down the camera origin for better framing
float3 rayOrigin = cameraOrigin * 0.001;
float3 rayDir = worldDir;
// Ray march
MarchResult finalResult = MarchFractal(rayOrigin, rayDir, power);
// Calculate color based on total steps taken
float stepFactor = saturate(finalResult.steps / float(MARCH_MAX_STEPS));
float t = sqrt(stepFactor);
// Gradient color stops
float3 almostBlack = float3(0.0, 0.0, 0.05);
float3 deepBlue = float3(0.05, 0.15, 0.3);
float3 vibrantBlue = float3(0.2, 0.4, 0.8);
float3 brightBlue = float3(0.6, 0.8, 1.0);
// Blend through gradient
float range1 = saturate(t / 0.15);
float range2 = saturate((t - 0.15) / 0.25);
float range3 = saturate((t - 0.4) / 0.6);
float3 fractalColor = lerp(lerp(lerp(almostBlack, deepBlue, range1), vibrantBlue, range2), brightBlue, range3);
// Add green tint based on total DE iterations
float iterFactor = saturate(finalResult.totalDEIterations / float(DE_MAX_ITER * finalResult.steps));
if (iterFactor > 0.1)
{
float greenAmount = (iterFactor - 0.1) / 0.9;
greenAmount = pow(greenAmount, 2.0);
fractalColor = lerp(fractalColor, float3(0.3, 0.5, 0.6), greenAmount);
}
if (iterFactor > 0.9)
{
float greenAmount = (iterFactor - 0.9) / 0.1;
greenAmount = pow(greenAmount, 5.0);
fractalColor = lerp(fractalColor, float3(0.3, 0.9, 0.7), greenAmount);
}
if (finalResult.hitStatus == HIT_STATUS_HIT)
{
return fractalColor;
}
else if (finalResult.hitStatus == HIT_STATUS_MISS_DISTANCE)
{
float fogAmount = pow(stepFactor, 0.1);
return lerp(sceneColor, fractalColor, fogAmount);
}
else
{
return fractalColor;
}
}
};
// Usage in material custom node:
// #include "/Project/FractalMaterial.usf"
// FractalFunctions FF;
// return FF.RenderFractal(ScreenPosition, SceneColor, Power).rgb;