CS348B Final Project: Custom Arcade Controller
David Schneider, Timothy Wong
Our goal was to produce a realistic image of Timothy's custom-made arcade-style joystick controller, shown below.
Arcade-style joystick controllers are popular among game enthusiasts who enjoy the genuine feeling of using arcade cabinet controls. Timothy had this particular controller custom-built for him by a vendor on the http://shoryuken.com/ forums according to his specifications. It features authentic Japanese arcade parts, including a Sanwa JLF joystick with pink Seimitsu LB-39 ball top, commonly referred to as a "bubble top".
Here is the main render we entered into the rendering competition. (Click for a larger version)
And here's a different camera angle, showing the detail in the bubble top. (Click for a larger version)
Bubble Top - Timothy Wong
Generating the Bubbles
The inside of the bubble top contains numerous miniature bubbles--much too many to model by hand. To generate positions for the our bubbles, we wrote a simple script that generated random candidate locations inside the main joystick ball, rejecting any that would cause overlaps in the bubbles. The output of the script is a .pbrt file with each individual bubble specified as a sphere shape, with the appropriate transformation. We also added a small amount of variance in the radius of the bubbles, to make them a little bit less uniform.
The distribution of the bubbles in the original joystick seems to be mostly uniform near the center of the bubble top, falling off as you approach the outside of the sphere. To emulate this, we used a piecewise PDF to determine the radius at which each bubble is located. The PDF is uniform from 0 to 75% of the radius of the sphere, and then drops off using an x^8 curve. Sampling the distribution involved determining the inverse CDF and then using the inversion method.
Our final scene used a total of 7500 individual bubbles.
Rendering the Bubbles
The main joystick ball is modeled as a glass sphere with index of refraction = 1.25. We tweaked the reflection and transmission parameters until we were able to achieve the appropriate shade of pink. Because of how pbrt handles refraction, we were able to simulate the air bubbles by modeling each bubble as a small glass sphere within this large sphere, using the reciprocal index of refraction, 1/1.25 = 0.8.
We used the photon mapping surface integrator to model reflection and transmission through the glass, which results in some very nice caustics where the light passes through the bubble top. We were forced to reduce the number of photons for the sake of rendering time, so there are some small artifacts that may be visible in the lighting of the scene.
The metal shaft of the joystick is modeled using pbrt's metal-type material, using spectral power distribution files that were included with pbrt's example scenes. The ridged head of the shaft inside the bubble top is a triangle mesh that we generated using another script, which generates a ridged cylinder (or partial cone) with an arbitrary number of ridges. The cardboard base of the stick is modeled using a matte material with bump mapping to give it the appropriate texture.
CSG (Constructive Solid Geometry) - David Schneider
Since neither of us can model in 3D, we wanted an easy and intuitive way to model the carefully machined controller parts. CSG made perfect sense for this task. Secondly, we wanted the implementation to be generic and robust enough such that it could potentially be incorporated into PBRT as-is.
Language Extension and Semantics
We wanted a fully-integrated solution, so extending PBRT's language in a minimalistic way seemed like the right approach. Shape is still used to create shapes, transforms are applied as usual, and CSG trees can be created inside object definitions. We decided not to allow instantiating an object when generating a CSG tree, as the semantics aren't well-defined.
A CSG tree is started using CsgBegin and finished using CsgEnd. The CsgBegin block behaves like a fully-confined shape instantiation. As such, making multiple levels of hierarchy is as simple as nesting a CsgBegin block inside another, and each CsgBegin and CsgEnd pushes and pops the attribute stack, so changing the CSG tree will not affect the surrounding tranformations.
CsgAnd, CsgOr, and CsgAndNot define the operation to apply using the following shape. There is precedence, so Not > And > Or, as in most programming languages.
Instead of specifying several of the same operation, a PBRT script writer can specify an implied operation along with the CsgBegin, in the form of CsgBegin "op", where op can be and, or, andnot, or the special mode close. Then, if two shapes are specified without a CSG operation in-between, the implied operation is automatically used. This allows for very compact and readable CSG trees. Not specifying an implied operator requires you to always specify an operation between all shapes; otherwise an error message is raised. Implied operations are not inherited in nested CsgBegin blocks.
There is one extra feature of this CSG implementation: CsgBegin "close". Many shapes in PBRT, such as heightfields, cylinders, cones, and so on, are implemented in its purest form. As such, since true CSG requires closed shapes to function correctly, many of the convenient, mathematically-defined, and optimized shapes in PBRT would not be suitable for use in CSG. Requiring the user to provide a closed triangle mesh or custom shape instead would eliminate much of the convenience of CSG. Instead, we provide the CsgBegin "close" mode. It behaves like CsgBegin "or", except it is assumed that the resulting node of the CSG tree will be a convex, closed union of the provided shapes. Using this, you can easily make a closed cylinder by combining a cylinder with two disks on the ends, or really anything that results in a simply closed, convex shape. It's essentially a more semantically-restricted OR that can be used to bend the CSG rules. Currently, the implementation completely ignores the orientation of shapes in close mode, which allows for very lazily-defined closed shapes. This may be the root cause of the only known bug in this CSG implementation, which can result in artifacts along the edges of CsgBegin "close"-defined shapes at very high resolutions. This can be easily fixed if needed.
That describes the full semantics of CSG. There are no limitations to the complexity of the shapes that make up the tree (as long as they are closed); crazy things like creating a heightfield cube using close and then boring out a killeroo-shaped hole (using CsgAndNot) is completely legitimate.
As an example, we rendered the classic CSG tree.
The model portion of the pbrt script follows. It intentionally uses multiple ways of specifying the CSG operations:
CsgBegin CsgBegin Material "plastic" "color Kd" [5 1 1] "color Ks" [50 50 50] Shape "trianglemesh" "point P" [ (0.5-cube points) ] "integer indices" [ (cube indices) ] Material "plastic" "color Kd" [0 0 5] "color Ks" [50 50 50] CsgAnd Shape "sphere" "float radius" 0.67 CsgEnd CsgAndNot CsgBegin "or" Material "plastic" "color Kd" [0 5 0] "color Ks" [50 50 50] CsgBegin "close" Rotate -90 1 0 0 Translate 0 0 -1 Shape "cylinder" "float radius" [0.35] "float zmin" [0.0] "float zmax" [2.0] Shape "disk" "float radius" [0.35] Translate 0 0 2 Shape "disk" "float radius" [0.35] CsgEnd CsgBegin "close" Rotate -90 0 1 0 Translate 0 0 -1 Shape "cylinder" "float radius" [0.35] "float zmin" [0.0] "float zmax" [2.0] Shape "disk" "float radius" [0.35] Translate 0 0 2 Shape "disk" "float radius" [0.35] CsgEnd CsgBegin "close" Rotate -90 0 0 1 Translate 0 0 -1 Shape "cylinder" "float radius" [0.35] "float zmin" [0.0] "float zmax" [2.0] Shape "disk" "float radius" [0.35] Translate 0 0 2 Shape "disk" "float radius" [0.35] CsgEnd CsgEnd CsgEnd
The resulting image is as you'd expect, complete with internal lighting:
For brevity, we'll skip the explanation of the parser changes; they're straightforward.
At the core of the implementation is the Csg class (which extends Aggregate, and as such is a Primitive), and its helper class, Csg::HitRegion.
Additionally, the BBox class was modified to have an intersection operation, along with convenience operators *, *=, +, +=, bool. The * and + operators could have been & and | in retrospect; these can of course be easily changed if incorporated into PBRT. The bool conversion operator returns true if the bounding box has strictly positive area.
Csg::HitRegion is the main computation. It has four major methods: Csg::HitRegion::Fill, Csg::HitRegion::Intersect, Csg::HitRegion::Merge, and Csg::HitRegion::Close.
Csg::HitRegion::Fill iteratively casts a ray into a supplied shape until Ray::maxt is surpassed, or the ray does not hit anything. The first ray cast always has a maxt of INFINITY, as the polarity of the node (whether the point is inside or outside of the shape) needs to be determined. The sign of the dot product of the ray's direction and the normal of the surface hit defines whether the collision exits or enters the shape. After the first ray cast, the origin, maxt and mint of the ray are updated, and further casts toggle the polarity of the region. For each hit (including the first), both the t value along the (original) ray and the resulting Intersection instance are saved in a vector. Whether the intersection begins or ends an "inside" region is left implied; only the polarity of the overall Csg::HitRegion needs to be stored. Note that even the second and later collisions have their normal checked against the ray, as only collisions with opposite normals should be saved. Collisions with the same normal sign as last time imply that the precision of the shape's intersection calculation was insufficient to advance beyond the previous intersection. Each advancement of the ray sets the origin to the collision location plus Intersection::rayEpsilon times a factor. The factor is initially 2 and is doubled each time the ray fails to escape the previous intersection (as signaled by the normal not changing in sign). This allows CSG to have extremely high precision when the shape allows, and yet quickly escape and advance despite poorly-implemented/-analyzed shape implementations or glancing-angle collisions.
Csg::HitRegion::Intersect and Csg::HitRegion::Merge perform straightforward intersections and unions of two Csg::HitRegion collision sets. Merging properly eliminates extra edges inside of shapes, so the inside of CSG shapes is very clean.
Csg::HitRegion::Close is used to create closed CSG nodes from open shapes, and as such assumes convexity, very quickly grabbing just the first and last intersections of the two Csg::HitRegions.
Each Csg instance then stores one set of Or sets of Ands, in the form of a vector of vectors. The fact that CSG syntactically requires more complicated combinations of operations to use nested CsgBegin blocks enables such a very simple implementation. Each And component also contains a boolean dictating whether to invert it or not; this results in a trivial inversion of the polarity of the Csg::HitRegion later on.
To execute Intersect and IntersectP, the Csg class uses Csg::HitRegion extensively. As such, the implementation is very straightforward; a Csg::HitRegion is maintained for each set of Ands, with each shape's Csg::HitRegion::Fill result being combined via intersection. If the shape was to be combined using AndNot, its Csg::HitRegion polarity is trivially flipped before intersecting. Finally, the resulting Csg::HitRegions are Merged together (or Closed, depending on the mode), and the closest intersection, if within Ray::maxt, is returned.
While not strictly necessary, nested Csg instances are detected (using C++'s dynamic_cast), and the native Intersect method is called (which directly returns a Csg::HitRegion), avoiding a lot of redundant CSG subtree evaluations.
BBoxes for each node (including aggregate And sets) are evaluated at construction time and are used to quickly eliminate parts of the tree. This also allows CSG to detect and report empty nodes (such as due to disjoint Anded shapes).
Or set evaluations early-out when called via IntersectP. This only applies to the root node of the CSG tree; subsequent levels cannot early-out in this way.
And set evaluations early-out, avoiding testing more nodes if the aggregate Csg::HitRegion has become empty.
Ray::maxt is used within And sets to try to avoid casting rays further than necessary (which can save a lot of time on rough or complex shapes where one ray may intersect many times). While the first ray cast into a shape must always have a maxt of INFINITY, subsequent rays need only go so far as the farthest t still included in the aggregate And set's Csg::HitRegion. Surprisingly, this optimization is effective even on simple shapes such as in the above example tree, speeding up rendering by about 5%. Note that this optimization does not guarantee the resulting Csg::HitRegion has no intersections with t values beyond maxt.
The code is available as a fairly non-invasive patch to PBRT.
Modeling the Controller - Timothy Wong
With the CSG API in place, we were able to model the rest of the controller surprisingly easily. Aside from the metal joystick shaft (described earlier), we didn't use any triangle meshes to model the controller, except for rectangular prisms (pbrt has no cube primitive).
The buttons were a perfect chance to utilize CSG, since they feature smooth, curved surfaces that would be difficult to approximate well using triangle meshes. We modeled them using pbrt's plastic material to get specular reflections.
The circular casing around each button is simply modeled by using a squashed sphere, andnot'ed with a cylinder to drill out its center. Note that the cylinder is constructed using CsgBegin "close".
The main button body is built from three parts, or'ed together. The bottom piece is a simple closed cylinder. The top piece is a large sphere and'ed with a cylinder to provide a slight curvature to the top of the button, and the middle is a squashed sphere andnot'ed with a closed cylinder to smoothly join the two. Again, all of the cylinders involved are made using CsgBegin "close".
The end result is a button with smooth, rounded edges, no matter how much you zoom in.
The rest of the controller is modeled using similar techniques. For example, the beveled edges on the white plastic frame are made by or'ing rectangular sheets with cylinders, and the hole for the controller's cable is made by drilling a cube and cylinder into the front of the controller's frame (using andnot).
Fortunately, Timothy still had the original image he designed for the custom artwork on the stick, so getting the same result on our model was as simple as dropping it in as a diffuse texture, with some minor gamma correction.
The pbrt scene files, along with associated scripts, are available here. To render, edit render.pbrt to select a set of rendering presets and use the included Makefile, which automatically runs the appropriate scripts and starts pbrt.
Modeling the controller was very simple after we had finished our CSG implementation. However, getting the photon mapping to look right proved to be a major challenge. As mentioned earlier, we had to reduce the number of photons used in our final render in the interest of time, so there's a bit of inaccuracy in the lighting. We had also wanted to model the glass pane covering the artwork in the real controller, but photon mapping doesn't seem to handle large sheets of glass well unless an unrealistically large number of caustic photons are used.
There were also some subtle CSG implementation issues, but they've all been ironed out after some careful reasoning. As mentioned before, artifacts can sometimes occur in shapes defined with CsgBegin "close" at high resolutions.