New Julia user seeking feedback on an in-progress project

Hi folks! I’m a new Julia user transitioning from Java. Thanks to the folks in the #helpdesk channel on the Slack, I’ve been learning a lot. I’ve started work on a port of the second chapter from Physically Based Rendering Techniques, which covers geometric structures, such as points, vectors, and normals, along with AABBs, transforms, and surface interations. Together, I think these form a fun challenge for writing idiomatic Julia.

Currently, my biggest challenges are:

  • Speeding up my testing workflow
  • Writing useful documentation
  • Avoiding constructor ambiguities

I’d love to hear from folks what they think works well in this package, if anything, and what they think is less useful or is questionable, design-wise.

Thanks everyone! The Julia community has been really welcoming these past two weeks, so I appreciate all the help I’ve already received from people.

8 Likes

Hi, thanks for posting and welcome. I think having a working physically based renderer would be both extremely interesting and also relevant to the community.

Implementing PBRT is an interesting case because the julia ecosystem is already highly numerical so I you’ll find many things which are required for a physically-based render are already available:

  • Short efficient arrays
  • Statistical distributions
  • Monte carlo integration
  • Geometric algorithms
  • Automatic differentiation
  • Image processing tools
  • The list goes on!

One thing you may have already noticed is that Julia is very composable: small focused libraries can be used together without much (or any) integration code. So “implementing PBRT” could be tackled in two rather different ways depending on your goals for this project:

  • A standalone translation of the C++ codebase. In a less composable/numerical language this would be the obvious approach. It allows you to keep reasonably close to the PBRT design and learn how to write some of the basic utilities in a Julian way.
  • A composition of existing numerical packages into a renderer project, creating new composable packages as required. This involves more redesign but would ultimately produce a more useful Julian result.

As an example of this tension, I can see that your Ray.jl has some code for ray differentials. My memory is a little hazy, but if I recall ray differentials are essentially forward mode autodiff applied to some part of the light transport? In that case you might get away with not writing that code, and instead use ForwardDiff.jl to handle the differentiation.

Anyway, sorry I don’t think this really answers your immediate questions at all, but asks a different one: do you feel more like staying close to the details of the PBRT code or using Julian tools to create the same functionality with a similar high level design but different details?

9 Likes

Hi Chris!

While my immediate questions weren’t answered, you asked a better one—where do I take this, and how? I think the ways you listed are both useful, but for different sets of goals. The first is more useful if you’re looking to learn more about physically based rendering and writing idiomatic Julia, while the second is more in line with the philosophy of Julia as a whole. I think both goals are important, and while I’m in the first camp more than the second, I don’t think it’s unreasonable to want a little bit of both.

Something I’ve been thinking might work: mixing a little bit of both approaches—implementing certain utilities where specific behavior is important, and composing the rest with existing Julia libraries. What are your thoughts on this? To a certain extent, I’m doing this already—as I build this library, I’m discovering that StaticArrays covers a lot of my use cases already for operations on collections. What I’d like to do is use that existing structure, but have granular control over which types can exhibit which behaviors, since PBRT allows for interoperability between data types in some cases but not others.

This means that when I’m structuring out parts of the translation, I’ll choose to structure it in a Julian way (i.e. by behavior) rather than following exactly how PBRT organizes its code. In the case of the ray differential tension, I would address ray differentials when I create (or use) the code that talks about differentiation, rather than with the Ray itself, since the scale_differentials function doesn’t “belong” to the Ray.

Thanks so much for the feedback! It’s given me a useful set of things to think about moving forward.

For sure, that’s very realistic. I guess my post above is a bit of a false dichotomy, but there’s some extent to which you’ll need decide how much to port the PBRT code for common utilities vs just use existing julia libraries. I guess using external libs could be seen as good or bad for an educational project. If you’re not scared to dive into the implementation of those libraries where necessary I think it can be very educational to see how other people achieve good performance and APIs. Though of course not all libraries are created equal in those respects. Personally I do spend a lot of time reading other people’s code but different people seem to have a different tolerance for that kind of thing.

As I wrote in #rendering on slack, you can expect a lot of low level geometric and numerical utilities to exist already. On the other hand, many task-specific utilities you’re likely to have to write yourself, including the high level application code. The high level code structure is where I’m guessing you’ll get the most benefit from following PBRT.

One question I have / possible suggestion for even further reducing code duplication is: why do you hard code the dimension of your types with things like CartesianPair / CartesianTriple?

Couldn’t you equivalently do something like

using StaticArrays

struct CartesianTuple{N, T}
    pts::SVector{N, T}
end

CartesianTuple{N}(args...) where {N} = CartesianTuple{N, eltype(SVector(args))}(SVector(args))

CartesianPair{  T} = CartesianTuple{2, T}
CartesianTriple{T} = CartesianTuple{3, T}

Base.getindex(t::CartesianTuple, inds...) = getindex(t.pts, inds...)

using Rematch
Base.getproperty(u::CartesianPair, s::Symbol) = @match s begin
        :x => u[1]
        :y => u[2]
        _  => getfield(u, s)
    end
end

Base.getproperty(u::CartesianTriple, s::Symbol) @match s begin
        :x => u[1]
        :y => u[2]
        :z => u[3]
        _  => getfield(u, s)
    end
end

and then everything works:

p = CartesianPair(1, 2)
t = CartesianPair(1, 2, 3)

julia> p.x
1

julia> t.z
3

The main advantage of this approach is that one could conceivably hijack all your machinery for use in higher dimensions pretty easily. Not sure how much practical value that has though.

1 Like

I like that approach! It means I can dictate indexing based on dimension, which could be very useful. I think it may also resolve some of the type weirdness I’ve been dealing with. I’ll try out this approach and let you know how it goes. Thanks!

1 Like