, cg
Houdini: Ray Tracer in VEX
I’ve seen many people sharing fun ray tracers experiments with Houdini’s VEX in the past,[1] and it just happens that I’ve had many ~10 min bursts of time to spare due to waiting for simulation computations, so I grabbed a copy of Peter Shirley’s mini book Ray Tracing in One Weekend[2] and gave it a go.
I’ve used Houdini facilities when possible, such as various SOP nodes to build the scene and the built-in VEX functions like intersect()
, fresnel()
, reflect()
, refract()
, and others, which allowed me to take a few shortcuts.
Hence it’s more of an adaptation rather than a 100% faithful implementation of the book, but the results are fairly similar.
Still, it’s been plenty enough to give a good introduction to ray tracing and I’m happy with that!
Performance-wise though... it’s not there yet. Probably a good contender for an implementation using an OpenCL SOP node?
The scene is available in the download link below, but I’ll also throw the VEX code here for quick reference:
#define MAX_BOUNCES 8
#define NEAR_DISTANCE 1.0e-3
#define FAR_DISTANCE 1.0e3
#define MATERIAL_LAMBERT 0
#define MATERIAL_METAL 1
#define MATERIAL_DIELECTRIC 2
#define GEOS_INPUT_IDX 1
#define SKY_RAMP_NAME "sky"
#define ALBEDO_ATTRIB_NAME "Cd"
#define NORMAL_ATTRIB_NAME "N"
#define MATERIAL_ATTRIB_NAME "material"
#define FUZZINESS_ATTRIB_NAME "fuzziness"
#define IOR_ATTRIB_NAME "ior"
struct Ray {
vector origin;
vector direction;
};
struct Hit {
int prim;
vector uvw;
vector pos;
vector normal;
};
void
trace(export int hitten; export Hit hit; const Ray ray)
{
vector pos;
vector uvw;
int prim;
prim = intersect(
GEOS_INPUT_IDX,
ray.origin
+ ray.direction * {NEAR_DISTANCE, NEAR_DISTANCE, NEAR_DISTANCE},
ray.direction * {FAR_DISTANCE, FAR_DISTANCE, FAR_DISTANCE},
pos,
uvw);
hitten = prim > -1;
if (hitten) {
hit.prim = prim;
hit.uvw = uvw;
hit.pos = pos;
hit.normal = primuv(
GEOS_INPUT_IDX, NORMAL_ATTRIB_NAME, hit.prim, hit.uvw);
}
}
void
evalLambertianMaterial(export int scattered;
export Ray scatteredRay;
export vector attenuation;
const Ray ray;
const Hit hit)
{
vector randomPos;
vector target;
randomPos = sample_sphere_uniform(set(nrandom(), nrandom(), nrandom()));
target = hit.pos + hit.normal + randomPos;
attenuation = primuv(GEOS_INPUT_IDX, ALBEDO_ATTRIB_NAME, hit.prim, hit.uvw);
scatteredRay = Ray(hit.pos, normalize(target - hit.pos));
scattered = 1;
}
void
evalMetallicMaterial(export int scattered;
export Ray scatteredRay;
export vector attenuation;
const Ray ray;
const Hit hit)
{
vector randomPos;
float fuzziness;
randomPos = sample_sphere_uniform(set(nrandom(), nrandom(), nrandom()));
fuzziness = min(
primuv(GEOS_INPUT_IDX, FUZZINESS_ATTRIB_NAME, hit.prim, hit.uvw), 1.0);
attenuation = primuv(GEOS_INPUT_IDX, ALBEDO_ATTRIB_NAME, hit.prim, hit.uvw);
scatteredRay = Ray(
hit.pos, reflect(ray.direction, hit.normal) + randomPos * fuzziness);
scattered = dot(scatteredRay.direction, hit.normal) > 0;
}
void
evalDielectricMaterial(export int scattered;
export Ray scatteredRay;
export vector attenuation;
const Ray ray;
const Hit hit)
{
float iDotN;
float ior;
float iorRatio;
vector outNormal;
float cosine;
float reflectedAmount;
float refractedAmount;
attenuation = {1.0, 1.0, 1.0};
scattered = 1;
iDotN = dot(ray.direction, hit.normal);
ior = ior = primuv(GEOS_INPUT_IDX, IOR_ATTRIB_NAME, hit.prim, hit.uvw);
if (iDotN < 0) {
// The ray comes from outside the surface.
iorRatio = 1.0 / ior;
outNormal = hit.normal;
cosine = -iDotN / length(ray.direction);
} else {
// The ray comes from inside the surface.
iorRatio = ior;
outNormal = -hit.normal;
cosine = ior * iDotN / length(ray.direction);
}
fresnel(
ray.direction, outNormal, iorRatio, reflectedAmount, refractedAmount);
if (nrandom() < reflectedAmount) {
scatteredRay = Ray(hit.pos, reflect(ray.direction, hit.normal));
return;
}
scatteredRay = Ray(hit.pos, refract(ray.direction, outNormal, iorRatio));
}
void
getColor(export vector color; const Ray ray)
{
int i;
Ray stack[];
vector accumulator[];
int hitten;
Hit hit;
i = 0;
push(stack, ray);
while (len(stack) > 0) {
Ray currentRay;
currentRay = pop(stack);
trace(hitten, hit, currentRay);
if (hitten && i < MAX_BOUNCES) {
int material;
int scattered;
Ray scatteredRay;
vector attenuation;
material = prim(GEOS_INPUT_IDX, MATERIAL_ATTRIB_NAME, hit.prim);
if (material == MATERIAL_LAMBERT) {
evalLambertianMaterial(
scattered, scatteredRay, attenuation, currentRay, hit);
} else if (material == MATERIAL_METAL) {
evalMetallicMaterial(
scattered, scatteredRay, attenuation, currentRay, hit);
} else if (material == MATERIAL_DIELECTRIC) {
evalDielectricMaterial(
scattered, scatteredRay, attenuation, currentRay, hit);
} else {
warning("unsupported material");
push(accumulator, {1.0, 0.078, 0.576});
break;
}
if (scattered) {
push(accumulator, attenuation);
push(stack, scatteredRay);
} else {
push(accumulator, {0.0, 0.0, 0.0});
}
++i;
} else {
float v;
v = dot(normalize(set(
currentRay.direction.x, 0.0, currentRay.direction.z)),
currentRay.direction);
v = currentRay.direction.y < 0 ? 0.0 : 1.0 - v;
push(accumulator, chramp(SKY_RAMP_NAME, v));
}
}
color = {1.0, 1.0, 1.0};
for (i = 0; i < len(accumulator); ++i) {
color *= accumulator[i];
}
}
void
getCameraRay(export Ray ray;
const vector throughPos;
const vector pos;
const vector u;
const vector v;
const float aperture)
{
vector2 randomPos;
vector origin;
randomPos = sample_circle_uniform(set(nrandom(), nrandom()));
randomPos *= aperture * 0.5;
origin = pos + u * randomPos.x + v * randomPos.y;
ray = Ray(origin, normalize(throughPos - origin));
}
cvex
main(export vector Cd = {0.0, 0.0, 0.0};
const vector P = {0.0, 0.0, 0.0};
const vector cameraPos = {0.0, 0.0, 0.0};
const vector cameraU = {1.0, 0.0, 0.0};
const vector cameraV = {0.0, 1.0, 0.0};
const float cameraAperture = 1.0)
{
Ray ray;
Cd = {0.0, 0.0, 0.0};
getCameraRay(ray, P, cameraPos, cameraU, cameraV, cameraAperture);
getColor(Cd, ray);
}
Download
This file contains a scene created with SideFX’s Houdini 16.5 as well as any required asset.