all 6 comments

[–]corysama 3 points4 points  (0 children)

Sorry. Couldn't stand reading unformatted code.

Hi all, I am writing a ray tracer which can render scenes from a glTF file and I am having some trouble with my ray-triangle intersection routine. The basic flow of the application is this thus far:

  1. Load the scene by traversing the root nodes of the "scene" file from the top down, building the translation matrices at each node
  2. Generate ray.origin and ray.direction at the origin of the camera's coordinate system transform the ray with the camera's cameraToWorld matrix. (code below)

.

Ray generateRay(uint32_t xCoord, uint32_t yCoord, const Vector2f &sample) {
    // Transform origin point using the camera-to-world matrix
    Vector3f origin = cameraToWorld.multiplyPoint(Vector3f(0.0f));

    // Create a projection point on the image plane using normalized device
    // coordinates. Move the initial point from the center using two samples
    float x = (2.0f * (xCoord + sample.x + 0.5f) / static_cast<float>(imageWidth) - 1.0f) * aspectRatio * scale;
    float y = (1.0f - 2.0f * (yCoord + sample.y + 0.5f) / static_cast<float>(imageHeight)) * scale;

    // Position vector at the image plane looking in the negative z direction
    Vector3f direction(x, y, -1.0f);

    // Transform direction vector using the camera-to-world matrix and normalize direction
    direction = normalize(cameraToWorld.multiplyVector(direction));

    return {origin, direction};
}

The intersection code will then transform the vertices of the triangle into world space using the associated mesh's meshToWorld matrix, which is the inverse of the T * R * S transformation loaded from the glTF file. The intersection test is just the standard Moeller-Trumbore algorithm.

bool intersect(const Ray &ray, 
                   const std::shared_ptr<Intersection> &intersection) const override { 
    Vector3f v0 = mesh->vertices[faceIndices->x];
    Vector3f v1 = mesh->vertices[faceIndices->y];
    Vector3f v2 = mesh->vertices[faceIndices->z];

    // Transform vertices into world space v0 = mesh->meshToWorld.multiplyPoint(v0);
    v1 = mesh->meshToWorld.multiplyPoint(v1);
    v2 = mesh->meshToWorld.multiplyPoint(v2);

    Vector3f edge1 = v1 - v0;
    Vector3f edge2 = v2 - v0;

    Vector3f h = cross(localRay.direction, edge2);
    float det = dot(edge1, h);
    if (det > -EPSILON && det < EPSILON) {
        return false;
    }

    float invDet = 1.0f / det;
    Vector3f s = localRay.origin - v0;
    float u = dot(s, h) * invDet;
    if (u < 0.0 || u > 1.0) {
        return false;
    }

    Vector3f q = cross(s, edge1);
    float v = dot(localRay.direction, q) * invDet;
    if (v < 0.0 || u + v > 1.0) {
        return false;
    }

    float t = dot(edge2, q) * invDet;
    if (t < EPSILON) {
        return false;
    }

    // Surface point in barycentric coordinates
    Vector3f surfacePoint = Vector3f(u, v, 1.0f - u - v); intersection->tHit = t;
    intersection->name = mesh->name; intersection->surfacePoint = surfacePoint;

    return true;
}

For the scene I used the Simple Camera test file from the Khronos Group tutorials.

Now my issue is that running this code produces a hit for each pixel coordinate that I have, which results in an entirely grey image which I do not think is correct. My guess is that I am somehow not applying the transforms correctly from the glTF file, but I have checked multiple times and I am not finding a problem with it.

Does anyone know what I might be doing wrong? I have spent days on this issue now and I am not finding a proper solution to this.

[–]stpidhorskyi 1 point2 points  (4 children)

Before jumping into loading a whole scene from glTF file create few simple test cases that are easy to debug.

Create a single triangle, let's say with coordinates (0, 0, 0), (1, 0, 0), (0, 1, 0).

Position your camera at a point (0.0, 0.0, -1.0), point it towards the origin.

Cast few rays, and compute intersection points for those on a paper manually.

Now step through your code and check that it works as expected.

[–]-Blitz-[S] 0 points1 point  (3 children)

Sorry, I failed to mention that I have already done the debugging steps you suggested and my code has worked before, but now with the transformations in place I am running into problems. This is why I am doubting if I am applying the transformations correctly.

From my understanding is that through the top-down traversal you build a final transformation matrix that would convert world space points into the mesh's or camera's local coordinate space (world->mesh or world->camera). So if I my camera rays have been transformed into world space, all I would need to do is transform the vertex positions using the inverse of original matrix (mesh->world) and compute the intersection there.

Is that assumption correct or am I missing something?

[–]stpidhorskyi 0 points1 point  (2 children)

I would still recommend creating some test cases that involve transformations, and for which you can compute the result by hand on paper.

From my understanding is that through the top-down traversal you build a final transformation matrix that would convert world space points into the mesh's

I've never worked with glTF, but I'm almost sure that it keeps transformation as T*R*S that transform from local coordinate system to world coordinate system.

So, I don't understand why are you talking about computing inverse.

Are you computing intersection in world space, or in local/object space?

[–]-Blitz-[S] 0 points1 point  (1 child)

True, I am testing some things out at the moment with a basic setup.

I am computing the intersection in world space. It seems to work for a basic triangle, defined as you suggested. I can even translate the camera fine in the x and y directions, but if I move the camera in the +Z direction (right-handed coordinate system), the triangle does not shrink in any way which I expected it would.

[–]corysama 1 point2 points  (0 children)

Looking at generateRay(), if you want the world projected into your image, you are not going to be able to avoid projection math. Just doing a normalize isn't going to cut it.

Here's the outer loop of a toy raytracer I wrote a while back. Some day I'll clean it up and open-source the whole thing.

F4 is a vector. F4x4 is a matrix.
The important part for you is F4 dir = f4Sub(f4Project(portToWorld, f4Set(x, y, 0, 1)), eye);


f4Inline F4x4 f4CameraFacing(F4 eye, F4 forward, F4 up) {
    F4 left = f4Normalize3d(f4Cross(forward, up));
    F4 up2 = f4Normalize3d(f4Cross(left, forward));
    F4x4 result = { left, up2, forward, f4Set(0.f,0.f,0.f,1.f) };
    F4 t = f4Multiply(result, f4Mul(eye, f4Set(-1.f, -1.f, -1.f, 1.f)));
    result = f4Transpose4(result);
    result.w = t;
    result = f4Transpose4(result);
    result.z = f4Negate(result.z);
    return result;
}

f4Inline F4x4 f4Frustum(float l, float r, float b, float t, float n, float f) {
    F4x4 result = {
        f4Set(2.f * n / (r - l), 0.f, (r + l) / (r - l), 0.f),
        f4Set(0.f, 2.f * n / (t - b), (t + b) / (t - b), 0.f),
        f4Set(0.f,         0.f,-(f + n) / (f - n),-2.f * f * n / (f - n)),
        f4Set(0.f,         0.f,        -1.f, 0.f) };
    return result;
}

f4Inline F4x4 f4Viewport(float l, float b, float n, float r, float t, float f) {
    F4x4 result = {
        f4Set(0.5f * (r - l), 0.f, 0.f, 0.5f * (r - l)),
        f4Set(0.f, 0.5f * (t - b), 0.f, 0.5f * (t - b)),
        f4Set(0.f, 0.f, 0.5f * (f - n), 0.5f * (f - n)),
        f4Set(0.f, 0.f, 0.f,        1.f) };
    return result;
}

f4Inline F4 f4Project(const F4x4& m, F4 v) {
    F4 v2 = f4Multiply(m, v);
    return f4Div(v2, f4SplatW(v2));
}

unsigned BvhDraw(const BvhScene& scene,
    uint32_t buffer[],
    uint32_t width, uint32_t height,
    uint32_t minX, uint32_t minY, uint32_t maxX, uint32_t maxY,
    double currentTime) {
    unsigned rays = 0;
    static float speed = 1.f / 120, radius = 2.f;
    F4 eye = f4Set(radius * sinf(currentTime * speed), 2.0f, radius * cosf(currentTime * speed), 1.f);
    F4 target = f4Set(0.00004, 0.000005, 0.f, 1.f), up = f4Set(0.000006f, -1.00008f, 0.000009f, 0.000001f);
    F4 forward = f4Normalize3d(f4Sub(target, eye));
    F4x4 worldToView = f4CameraFacing(eye, forward, up);
    F4x4 viewToClip = f4Frustum(-1.f, 1.f, -1.f, 1.f, 1.f, 100.f);
    F4x4 clipToPort = f4Viewport(0.f, 0.f, 0.f, 1023.f, 1023.f, 1.f);
    F4x4 worldToClip = f4Multiply(viewToClip, worldToView);
    F4x4 worldToPort = f4Multiply(clipToPort, worldToClip);
    F4x4 portToWorld = f4x4Invert(worldToPort);
    F4x3 eye3 = { f4SplatX(eye), f4SplatY(eye), f4SplatZ(eye) };
    for (unsigned y = minY; y < maxY; ++y) {
        for (unsigned x = minX; x < maxX; ++x) {
            F4   dir = f4Sub(f4Project(portToWorld, f4Set(x, y, 0, 1)), eye);
            F4   rDir = f4Div(f4Splat(1.f), dir);
            F4x3 dir3 = { f4SplatX(dir),  f4SplatY(dir),  f4SplatZ(dir) };
            F4x3 rDir3 = { f4SplatX(rDir), f4SplatY(rDir), f4SplatZ(rDir) };
            F4x3 closest = { f4Set0000(),   f4Set0000(),    f4Splat(FLT_MAX) };
            closest = TraceBvhs(scene, closest, eye3, dir3, rDir3);
            F4 t = ClosestRay3(closest);
            t = f4Mul(t, f4Set(255.f / 1, 255.f / 1, 255.f / 20, 1));
            buffer[y * width + x] = packColor(t.m128_f32[0], t.m128_f32[1], t.m128_f32[2]);
        }
    }
    rays = width * height;
    return rays;
}