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.

Ray Tracer in VEXWatch on Vimeo

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!

Rendering Plane

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);
}