Skip to content

Instantly share code, notes, and snippets.

@cnlohr
Created March 30, 2026 10:33
Show Gist options
  • Select an option

  • Save cnlohr/fa55a462476b0e1a2b2cad1d469eae62 to your computer and use it in GitHub Desktop.

Select an option

Save cnlohr/fa55a462476b0e1a2b2cad1d469eae62 to your computer and use it in GitHub Desktop.
Fixed-step object tracer in Unity Shader code
Shader "cnlohr/StepShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_TANoiseTex ("TANoise", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 5.0
#pragma multi_compile_fog
#pragma enable_d3d11_debug_symbols
//#pragma skip_optimizations opengl
//#pragma skip_optimizations d3d11
#include "UnityCG.cginc"
#include "/Assets/cnlohr/Shaders/tanoise/tanoise.cginc"
#include "/Assets/cnlohr/Shaders/hashwithoutsine/hashwithoutsine.cginc"
//#define glsl_mod(x,y) (((x)-(y)*floor((x)/(y))))
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 test : TEST;
float3 rayOrigin : RAY_ORIGIN;
float3 rayDir : RAY_DIR;
float3 worldPos : WORLD_POS;
float3 worldNormal : WORLD_NORMAL;
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
// https://github.com/cnlohr/shadertrixx?tab=readme-ov-file#raycasting-with-orthographic-and-normal-cameras
// I saw these ortho shadow substitutions in a few places, but bgolus explains them
// https://bgolus.medium.com/rendering-a-sphere-on-a-quad-13c92025570c
float howOrtho = UNITY_MATRIX_P._m33; // instead of unity_OrthoParams.w
float3 worldSpaceCameraPos = UNITY_MATRIX_I_V._m03_m13_m23; // instead of _WorldSpaceCameraPos
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
float3 cameraToVertex = worldPos - worldSpaceCameraPos;
float3 orthoFwd = -UNITY_MATRIX_I_V._m02_m12_m22; // often seen: -UNITY_MATRIX_V[2].xyz;
float3 orthoRayDir = orthoFwd * dot(cameraToVertex, orthoFwd);
// start from the camera plane (can also just start from o.vertex if your scene is contained within the geometry)
float3 orthoCameraPos = worldPos - orthoRayDir;
o.rayOrigin = lerp(worldSpaceCameraPos, orthoCameraPos, howOrtho );
o.rayDir = normalize( lerp( cameraToVertex, orthoRayDir, howOrtho ) );
o.worldPos = worldPos;
o.test = howOrtho;
o.worldNormal = mul(unity_ObjectToWorld, v.normal);
return o;
}
float4 SphereFunction ( float3 pos )
{
return float4( 1.0.xxx, 1.0-saturate( length( pos.xyz ) ) );
}
float4 MultiSphereFunction ( float3 pos )
{
return float4( 1.0.xxx, 1.0-saturate( length( glsl_mod(pos.xyz,1.0)-0.5 ) ) );
}
float4 ColorMultiSphereFunction ( float3 pos )
{
return float4( chash33(floor(pos.xyz)), 1.0-saturate( length( glsl_mod(pos.xyz,1.0)-0.5 ) ) );
}
float4 ColorMovingMultiSphereFunction ( float3 pos )
{
float3 sphid = floor(pos.xyz);
float4 spho = tanoise4_hq( float4( sphid, glsl_mod( _Time.y, 1000 ) ) );
return float4( chash33(sphid), 1.0-saturate( length( glsl_mod(pos.xyz,1.0)-0.5 - (spho.xyz-0.5)*0.5 ) ) );
}
float4 VoroiSphereSlave( int3 sphid, float3 relPlace )
{
float4 spho = tanoise4_hq( float4( sphid, glsl_mod( _Time.y*0.1, 1000 ) ) );
float4 ret = float4( chash33(sphid), 1.0-saturate(2.0*length( relPlace.xyz+(spho.xyz-0.5) ) ) );
//float4 ret = float4( sphid&1, saturate(1.0-(2.0*length(relPlace))));
ret.xyz *= ret.w;
return ret;
}
float4 MetaBall( int mb, float3 pos )
{
float4 spho = tanoise2( float2( mb*3.0, glsl_mod( _Time.y, 1000 ) ) );
float4 ret = float4( chash31(mb), 1.0-saturate(2.0*length( pos.xyz + spho.xyz ) ) );
ret.xyz *= ret.w;
return ret;
}
float4 ThreeBalls ( float3 pos )
{
// At each point, look for 8 closest spheres and create a metasurface.
pos /= 3.0;
pos -= 0.5;
float4 sum = MetaBall( 1, pos );
sum += MetaBall( 2, pos );
sum += MetaBall( 3, pos );
sum.a += 0.7;
return sum;
}
float sdTorus( float3 p, float2 t )
{
float2 q = float2(length(p.xz)-t.x,p.y);
return length(q)-t.y;
}
float4 Torus( float3 pos )
{
// At each point, look for 8 closest spheres and create a metasurface.
//pos /= 3.0;
float4 ret = float4( 1.0.xxx, 1.0-sdTorus( pos, float2( 1.0, -0.50 ) ) );
ret.rgb *= ret.a;
ret.a += 0.7;
return ret;
}
float4 frag (v2f i) : SV_Target
{
float4 col = 0.;
int step;
float3 rayDir = normalize( i.worldPos - i.rayOrigin );
//normalize( i.rayDir );
const float maxDepth = 15.0;
const int maxSteps = 100;
float distancePerStep = maxDepth / maxSteps;
float stepSpeedFromGlancingAngle = -1.0 / dot( i.worldNormal, rayDir );
float3 rayDirStep = rayDir * distancePerStep
;
// * stepSpeedFromGlancingAngle;
float3 pos = i.worldPos;
// Check world position
//return float4( glsl_mod( pos, 1.0 ), 1.0 );
//return float4( rayDir, 1.0 );
float opacity = 0.0;
float distanceRemain = 1.0;
float nearDerate = 0.0;
for( step = 0; step < maxSteps; step++ )
{
//float4 value = float4( 1.0, 1.0, 1.0, tanoise4_1d( float4( pos*1.0, _Time.y*0.1) ) );
//float4 value = tanoise4_hq( float4( pos*1.0, glsl_mod( _Time.y*0.5, 1000.0) ) );
//float4 value = SphereFunction( pos );
//float4 value = MultiSphereFunction( pos );
//float4 value = ColorMultiSphereFunction( pos );
//float4 value = ColorMovingMultiSphereFunction( pos );
//float4 value = VoronoiSpheres( pos );
//float4 value = ThreeBalls( pos );
float4 value = Torus( pos );
float nearDen = saturate( ( value.a - 0.7 ) * 5.0 );
// If very close to the glass, derate.
nearDen *= saturate( nearDerate );
nearDerate += 4.0 / maxSteps;
pos += rayDirStep;
col += (nearDen) * saturate(1.0 - opacity ) * distanceRemain * float4( value.rgb, 1.0 );
opacity += nearDen*0.5;
distanceRemain -= 1.0/maxSteps;
if( opacity > 1.2 ) break;
}
col = col;
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment