Hello,
i am currently trying to implement a Sphere-Tracing renderer. This is my first Julia project. So i lack on certain aspects like meta programming which is where i need your input.
It uses a cool concept called Signed distance functions. The idea is that you describe the world implicitly by a function which gives the shortest to any object (and if it is inside the -1* the shortest distance to the wall of the object).
My current problem is to find an efficient way to “compile” down a tree of 3d objects to a signed distance function that describes the whole world (or way to compose partials).
I like to give an overview of my attempts so far first. The source code is inline
- Hand coding This is a circle of radius 50 centered at point (1,1,1)
function scene(Pos::SVector{3})
norm(Pos-SVector{3,Float64}(1.0,1.0,1.0))-50.0
end
Full source of minimal example
- Functions of functions
function scene_ff(Pos)
union(trans(sphere(3.0,spot(SVector{3,Float64}(0,0,0),1.5,Red)),SVector{3,Float64}(0,20,0)),
plane(SVector{3,Float64}(0.0,0.0,1.0),checkerbox(10,Grey,Black)))(Pos)
end
where i divide in to Sdf and shaders
##Call signature of a shader
(Pos::SVector{3},Normal::SVector{3},Sdf,Step,Inital) -> x::RGB
## Call signature of a Sdf, rand is necessary hack, please ignore.
(Pos::SVector{3}) -> (distance to Sdf,rand(),Shader of that Sdf)
which are anonymous functions built by code like:
function union(SDFs...)
return (Pos::SVector{3}) -> minimum(map(x->x(Pos),SDFs))
end
function trans(Sdf,Vec)
return (Pos::SVector{3})->(Sdf(Pos-Vec))
end
function plane(Normal::SVector{3},Shader)
return (Pos::SVector{3}) -> (dot(Pos,Normal),rand(),Shader)
end
function sphere(Radius::Float64,Shader)
a = rand() #Needed for strict ordering since my lambdas/shaders can't be compared
return (Pos::SVector{3}) -> return (norm(Pos)-Radius,a,Shader)
end
function unicolor(Color::RGB)
return (Pos,Normal,Sdf,Step,Inital)->Color
end
function checkerbox(BSize,Shader1,Shader2)
return (Pos::SVector{3},Normal::SVector{3},Sdf,Step,Inital) -> if xor(mod(Pos[1],2*BSize)>BSize,mod(Pos[2],2*BSize)>BSize,mod(Pos[3],2*BSize)>BSize) Shader1(Pos,Normal,Sdf,Step,Inital) else Shader2(Pos,Normal,Sdf,Step,Inital) end
end
...
const Red = unicolor(RGB{Normed{UInt8,8}}(0.8,0.0,0.0))
This approach results in lot’s of allocations and does not lend itself to GPU’s which is my long term goal. But is really flexible
Full source with same scene ToO approach
- Tree of Objects parsed each time.
const scene = Conjun([
Transl(SVector{3,Float64}(0,20,0),Sphere(3.0,Spot(SVector{3,Float64}(0,0,0),1.5,Red))),
Plane(SVector(0.0,0.0,1.0),Checkbox(10,Grey,Black))
])
with Sdfs working in this manner:
struct Conjun <: Sdf
Coll::Array{Sdf, 1}
end
function dist(Sdf::Conjun, Position::SVector{3,Float64})
min(map(x->dist(x,Position),Sdf.Coll)...)
end
function closest(Sdf::Conjun, Position::SVector{3,Float64})
closest(Sdf.Coll[argmin(map(x->dist(x,Position),Sdf.Coll))],Position)
end
struct Transl <: Sdf
Off::SVector{3,Float64}
Vict::Sdf
end
function dist(Sdf::Transl, Position::SVector{3,Float64})
dist(Sdf.Vict,Position-Sdf.Off)
end
function closest(Sdf::Transl, Position::SVector{3,Float64})
closest(Sdf.Vict,Position-Sdf.Off)
end
abstract type SSdf <: Sdf end
struct Sphere <: SSdf
Radius::Float64
Shadr::Shader
end
function dist(Sph::Sphere, Position::SVector{3,Float64})
norm(Position)-Sph.Radius
end
function color(Sph::Sphere, Ri::RayInfo)
shade(Sph.Shadr,Ri)
end
And shader this way:
struct ConstantShader <: Shader
Color::RGB
end
function shade(Shadr::ConstantShader,Ri::RayInfo)
Shadr.Color
end
struct Spot <: Shader
lightPos::SVector{3,Float64}
brightness::Float64
Shd1::Shader
end
function shade(Shadr::Spot,Ri::RayInfo)
shade(Shadr.Shd1,Ri)*min(1,max(0.1,Shadr.brightness*dot(gradient(Ri.tSdf,Ri),normalize(Shadr.lightPos-Ri.Position))))
end
As you see many objects contain references to abstract types. This makes it even slower (although) less allocation heavy than the function of functions approach. It will not work well for GPUs either.
Full source same scene as fof
Problem:
Find a way to compile a scene graph down to a representation with three properties:
- It has a signed distance functino which gives you the scene you want (that’s easy) Learn why
- It has to tell my which shader to use for which object (this is where the hard coding approach failed badly)
- It has to be able to run on GPU
My idea is to built a tree description (from “Objects”) and have it compile down to look up tables and functions. Since that probably works good on GPU. But i am open for better ideas.
The scene after compilation would something like this:
struct Scene
Objects::SVector ## Function of the sdf of the different objects in absolute coordinates
Shaders::SVector ## Shaders[n_id] belongs to Objects[n_id]
mapID::Function ## Gives the id of most appropiate shader at position
mapObjc::Function ## Gives the id of the closest object from position
function Scene(Tree::Sdf)
#magic happens here
end
end
function dist (S:Scene,Pos)
## Maybe optimizations which not to look at but most likely min of all sfds
end
function color (S:Scene, Pos)::RGB
## does call shader, gives normal in respect to the closest Object and not the total SDF
This approach has several problems too:
- Can’t run on GPU since mapID functions make scene non isbits (maybe fatal for this approach)
- Needs magic/macros which converts an SDF tree in to a group of functions and look up tables
- I do not know how Julia macros work really
I would really appreciate your help/input/ideas.
Sincerely,
freemint