A Ruby gem to generate scene description files for the third edition of Physically Based Rendering.
This gem implements its file format specification and wraps it in a friendly DSL.
This gem makes it easier to generate scene description files, because:
- It provides methods that only accept valid parameter names
- It knows the types of parameters and automatically adds them for you
- It closely resembles the structure in the documentation
- It's easy to script with bits of Ruby
- It has no dependencies and streams to a plain IO object
This generates the example from the documentation:
require "pbrt"
builder = PBRT::Builder.new do
look_at(3, 4, 1.5, 0.5, 0.5, 0, 0, 0, 1)
camera.perspective(fov: 45)
sampler.halton(pixelsamples: 128)
integrator.path
film.image(filename: "simple.png", xresolution: 400, yresolution: 400)
world_begin do
comment "uniform blue-ish illumination from all directions"
light_source.infinite(L: rgb(0.4, 0.45, 0.5))
comment "approximate the sun"
light_source.distant(from: [-30, 40, 100], L: blackbody(3000, 1.5))
attribute_begin do
material.glass
shape.sphere(radius: 1)
end
attribute_begin do
texture("checks").spectrum.checkerboard(
uscale: [8],
vscale: [8],
tex1: rgb(0.1, 0.1, 0.1),
tex2: rgb(0.8, 0.8, 0.8),
)
material.matte(Kd: texture("checks"))
translate(0, 0, -1)
shape.trianglemesh(
indices: [0, 1, 2, 0, 2, 3],
P: [-20, -20, 0, 20, -20, 0, 20, 20, 0, -20, 20, 0],
st: [0, 0, 1, 0, 1, 1, 0, 1],
)
end
end
end
puts builder.to_s
This is what it looks like when rendered.
As you can see in the example above, the gem has a PBRT::Builder
that takes a block
where you can call methods to generate 'directives'. The methods you can call directly
correspond to the documentation.
For example, in the Transformations section
there is an 'Identity' directive which you can generate with the identity
method.
There are two kinds of directives:
- Those that take plain arguments
- Those that take arguments as named parameter lists
For example:
# plain arguments
translate(1, 2, 3)
# parameter list
shape.sphere(radius: 2, zmin: 0.2, zmax: 0.7)
The majority of directives have 'implementations' such as the 'perspective' implementation for the 'Camera' directive or the 'sphere' implementation for the 'Shape' directive. To specify this, call the method on the directive:
camera.perspective
sampler.halton(pixelsamples: 16)
shape.sphere(radius: 1)
light_source.spotlight(from: [0, 1, 0], to: [0, 0, 0])
All directives and implementations are specified in the documentation as well as the names
of parameters and their types. For parameters that have type point2
, point3
, vector2
etc,
you can pass an array and if the parameter takes several points e.g. point[4]
you can either
pass a flat array or group the individual points into sub-arrays:
# flat array
shape.curve(P: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
# sub-arrays
shape.curve(P: [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]])
There is no difference (other than readability). They will generate the same code. In fact, the
same is true for any parameter that takes multiple values. For example, the
LookAt
directive takes 9 floats that may
be grouped into sub-arrays for clarity:
look_at(
[3, 4, 1.5], # eye
[0.5, 0.5, 0], # look at point
[0, 0, 1], # up vector
)
Some directives come in pairs like WorldBegin
/ WorldEnd
and AttributeBegin
/ AttributeEnd
.
The gem only provides a method for 'begin' and lets you pass a block to specify what goes inside it:
world_begin do
transform(1, 2, 3)
sphere.shape
attribute_begin do
# ...
end
end
Finally, there are some directives that take a name such as Texture
and MakeNamedMaterial
. In these cases, pass the name in to the
top-level method before calling others on it.
Textures also take a 'class' which is either 'spectrum' or 'float' which can be specified with a chained method call:
texture("mytexture").spectrum.checkerboard(dimension: 2)
make_named_material("mymaterial").plastic(roughness: 0.1)
If you can't figure out how to call the directive / implementation you want, this gem has an example of every single
one being called in it's builder_spec.rb
.
Some parameters take types that are spectrum
, spectrum texture
, float texture
, spectrum / float texture
.
A spectrum type is used to specify a color spectrum which can be represented in different ways. Parameter Lists section for more details. When a parameter has the spectrum type, wrap its arguments with one of the representations:
# rgb
light_source.point(scale: rgb(0.8, 0.1, 0.1))
# xyz
light_source.point(scale: xyz(0.8, 0.1, 0.1))
# sampled
light_source.point(scale: sampled([300, 0.3, 400, 0.6])
light_source.point(scale: sampled("filename"))
# blackbody
light_source.point(scale: blackbody(6500, 1))
Occasionally this can be omitted if it can be inferred:
light_source.point(scale: "filename")
But it's usually better to include it to make your intent clearer.
A type like spectrum texture
actually means the parameter accepts either a spectrum or the name of a texture
that you have created with the 'Texture' directive. In this case, the string "filename" is ambiguous because it could
be a file containing spectrum sample data, or it could be the name of one of your textures.
If things are ambiguous, PBRT will raise an error:
AmbiguousArgumentError:
Please specify whether "filename" is a spectrum or texture.
If it's a texture, wrap it with: texture("filename")
If it's a spectrum, wrap it with its representation: sampled("filename")
Valid representations are: rgb, xyz, sampled and blackbody
For the float texture
type, it can always be decided what you meant because a float
is always a number and a
texture
is always a string.
There's only one case where the type can be ambiguous and PBRT will not raise an error. If you enter a number for the
spectrum / float texture
, PBRT will assume you meant a float rather than a spectrum as floats are much more common, but
if you really want a spectrum then wrap the argument in one of its representations (e.g. rgb).
In the example above, a block is being passed to the PBRT::Builder
, but you can also use this as a more traditional
builder by calling methods on it:
builder = PBRT::Builder.new
builder.world_begin do
builder.translate(1, 2, 3)
builder.shape.sphere
end
Builder methods return self
so you can chain methods if you'd like:
translate(1, 2, 3).shape.sphere(radius: 1)
When a builder is constructed, it takes an IO object that it streams its directives to. You can pass one of your own, for example, if you want to stream your directives straight into a file to save on memory usage:
File.open("myscene.pbrt", "w") do |file|
PBRT::Builder.new(io: file) do
translate(1, 2, 3)
shape.sphere(radius: 1)
end
end
This gem does some basic checking of parameter and can infer types for you, but it won't do much beyond that. Specifically:
- It won't error if you pass values of the wrong type
- It won't error if you pass arrays with too many / few values
- It won't error if you use directives inappropriately, e.g. specifying
LookAt
inside theWorldBegin
section - It won't explain what any of the directives mean or how to use them
It would be great if it did some of those things, and I'd happily welcome pull requests to add them.
- The 02sequence sampler is called o2sequence because Ruby methods can't begin with a number
- The texture method can be used for adding a directive as well as disambiguating values
- Every single material parameter can be used in shape directives because the specification allows it
- You can actually generate multiple renders for the same scene by adding more
WorldBegin
directives
I am in no way affiliated with PBRT or any of its authors.
If you find this gem useful, I'd love to hear how you're using it on Twitter and will happily welcome pull requests or suggestions for improvement.
I intend to use this on some other projects of my own, so a search of my GitHub repositories might be useful if you're planning to use it. Good luck.