Reputation: 496
I am currently take a course on production graphics and since we are spend the whole semester building a raytracer I would like to experiment as much as possible. I used to use C++ but it gets really messy at some point. And I trust haskell as a very good language for prototyping, and clean code. However, I would really appreciate some advice as now I could not find a way for data representation.
Here is currently what I thought.
A raytracer in general is a scene
as an environment so I might use a monad (build upon state monad probably) to connect different part in the tracer.
Camera
is the simplest. It has Position and Direction and can be moved around and rotate.
The other parts become tricky. I wish to have Screen
which could either generate pinhole or depth of field, and a list of lights
that could be environment
, point light
, area light
or directional light
. The objects in the scene
is made up by material
and geometry
where material might have phong
or glossy
or transparent
so on and so forth, and geometry
could be sphere
triangle
plane
or a bunch of them (I am thinking bhv
or kdtree
as representation)
So, if I am in C++, I am pretty sure I will use inheritance to express such polymorphism. However, I am stuck on how should I represent that in haskell.
My initial attempt (if I do not have to consider secondary ray at all) is to use typeclass
. So I have something like this
class Geometry geo where
intersect :: geo -> Ray -> Intersection
class Material mat where
shade :: (Light l) => mat -> Intersection -> l -> Color
And so I define my Objects in the following way. I looked up and I tend to use RankNTypes
(I think I made a mistake here)
data Object = forall . geo mat (Geometry geo, Material mat) => Object geo mat
Then I found I could not go on if I plan to implement the following function.
raytrace_objects :: (Material mat) => Ray -> [Objects] -> (Intersection, mat)
raytrace_objects ray objects =
foldl reduce (farest_intersection, blackbody) objects
where
reduce (Object geo mat) old@(old_inter, _) =
if new_inter < old_inter
then (new_inter, mat)
else old
where
new_inter = intersect ray geo
And blackbody
just return black for the shading function
Suddenly I found that it is impossible!!!! mat
has to match the material type whatever the nearest objects has. And it is not decidable during the static analysis. So, I am stuck. Is there any suggestion how I could surpass the haskell's type system or is there a better way to do it?
Upvotes: 0
Views: 248
Reputation: 789
As a beginner you should try to avoid language extensions, because they can mislead you to less ideomatic solutions.
In Haskell, you sometimes rely on functions for polymorphism where you would use inheritance in other languages. Consider this simple sketch, which prefers functions over "objects":
type Geometry = Ray -> Intersection
type Material = UV -> Color
data Object = Object { objGeometry :: Geometry
, objMaterial :: Material
}
blackbody :: Material
blackbody uv = RGB 0 0 0
sphere :: Double -> Geometry
sphere radius ray = ...
Of course, to get reasonable performance, this would need to be extended with bounding boxes and other optimizations. Still, it shows that you can approach the problem from a completely different side.
This looks more like a proper type for Object
:
data Object geo mat = Object ...
The constraint (Geometry geo, Material mat) =>
should instead go on the functions which handle Object
, and only when they actually need it.
raytrace_objects
will not work this way, because the objects
parameter is of type list ([]
). All elements in a list must be of the same type. This type is fixed at compile time. Object geo mat
is not a plain type, because the parameters are not yet known. So there cannot be any actual values of this type. Object Sphere Wood
might be a suitable type. Thus, [Object Sphere Wood]
is a list that can only contain wooden spheres.
Food for thought and search engines: Object geo mat
(which is a short notation for forall geo mat. Object geo mat
) is a higher-order type which has kind * -> * -> *
. [x]
has kind * -> *
, and [Int]
has kind *
.
This is a key difference between algebraic data types with type-classes in contrast to object-oriented programming: The Haskell compiler needs to know all types at compile time, whereas OO languages typically only need to know an interface and do not care about the actual runtime type.
Higher-order types and type-classes are more like templates in C++.
You could solve your problem at hand with heterogenous lists (e.g. HList
from Hackage), which work more like OO lists in the sense that they can accept objects of varying types. Or you try more idiomatic solutions like the one outlined above.
Upvotes: 1