Hello USD - Part 22: The Control Flow is a lie.
This article is part of a series.
Continued from last post, a fleshed out protocol based result builder now drives SketchPad! It took some ironing out to get full parity with the [Sphere]
based one, especially the for loops. I’m happy as the new code puts me in good shape to add new features, but I don’t love it yet.
The actual proposal on result builders highlights just how much cleverness backs them up, but also, on close reading, why they seem to be so fiddly as soon as generics and protocols come into play.
The key detail – anything that looks like “control flow” in a Result Builder… isn’t. The purported “for…in” is just a closure that must have a single return. NOT a real code loop AT ALL. No break
, no continue
… nothing for-loopy about them at all. Just a sad sad zombie for loop. A foreach! I’m considering taking them out of the LayerBuilder entirely like SwiftUI does because they feel so misleading in a creative coding context.
In addition, the data structures the Protocol-Builder pair produces are just chock full of glue-layers that really aught to be melted out like the support material on a 3D print. Want an if statement, now you need an _Wrapped
struct. Want that if/else statement, now you need a _Either
struct. Want a for loop, now you need a _Array
struct. StringNode
’s resultBuilder needed none of that because it has a concrete type backing it up. Winnowing the scaffolding and returning something more ordered could be the job of a buildFinalResult
function perhaps, but in the mean time I’ll just load out the USD files as the “source of truth”.
To have a record and for experimentation, I made a version of the code that isolates out Layers
and the LayerBuilder
with a render
function that prints to the console. (runs in Playground)
At the bottom, a IndexLoop()
function addresses some of my concerns with the default for
implementation. It could be rewritten with sequence, to take in a range, etc.
struct IndexLoop<Content:Layer>:Layer, RenderableLayer {
var id: String { "Repeating" }
//var count:Int
var elements: [Content]
public init(from start:Int = 0, to limit:Int, by increment:Int = 1, @LayerBuilder content: (Int) -> Content) {
self.elements = []
for index in stride(from: start, to: limit, by: increment) {
self.elements.append(content(index))
}
}
func render() {
for element in elements {
element._render()
}
}
}
let multiType2 = Assembly {
Square()
IndexLoop(to: 3) { index in
let adjusted = index+2
CircleWithParam(radius: adjusted*3)
SquareWithParam(side: adjusted*2)
}
Circle()
}
The resulting print out:
Assembly<Tuple2Layer<Tuple2Layer<Square, IndexLoop<Tuple2Layer<CircleWithParam, SquareWithParam>>>, Circle>>(content: __lldb_expr_15.Tuple2Layer<__lldb_expr_15.Tuple2Layer<__lldb_expr_15.Square, __lldb_expr_15.IndexLoop<__lldb_expr_15.Tuple2Layer<__lldb_expr_15.CircleWithParam, __lldb_expr_15.SquareWithParam>>>, __lldb_expr_15.Circle>(first: __lldb_expr_15.Tuple2Layer<__lldb_expr_15.Square, __lldb_expr_15.IndexLoop<__lldb_expr_15.Tuple2Layer<__lldb_expr_15.CircleWithParam, __lldb_expr_15.SquareWithParam>>>(first: __lldb_expr_15.Square(), second: __lldb_expr_15.IndexLoop<__lldb_expr_15.Tuple2Layer<__lldb_expr_15.CircleWithParam, __lldb_expr_15.SquareWithParam>>(elements: [__lldb_expr_15.Tuple2Layer<__lldb_expr_15.CircleWithParam, __lldb_expr_15.SquareWithParam>(first: __lldb_expr_15.CircleWithParam(radius: 6), second: __lldb_expr_15.SquareWithParam(side: 4)), __lldb_expr_15.Tuple2Layer<__lldb_expr_15.CircleWithParam, __lldb_expr_15.SquareWithParam>(first: __lldb_expr_15.CircleWithParam(radius: 9), second: __lldb_expr_15.SquareWithParam(side: 6)), __lldb_expr_15.Tuple2Layer<__lldb_expr_15.CircleWithParam, __lldb_expr_15.SquareWithParam>(first: __lldb_expr_15.CircleWithParam(radius: 12), second: __lldb_expr_15.SquareWithParam(side: 8))])), second: __lldb_expr_15.Circle()))
Square
Circle with radius 6
Square with side 4
Circle with radius 9
Square with side 6
Circle with radius 12
Square with side 8
Circle
Updating the Builders
As the printout illustrates so well, the new result builder doesn’t have a pure representation of the 3D scene. Instead the "Stage"
Layer
must be traversed with functions that:
- A) know how to ignore the glue-structs
- B) know how to return a
StringNode
This change also improves the code’s architecture. Instead of a USDFileBuilder
with a buildSphere()
function, I now have a Sphere that conforms to a USDRenderable
protocol with a forUSDA()
function with same output.
extension Sphere {
func render(context: RenderContext) -> RenderContext {
context + [self.forUSDA()]
}
}
extension Sphere:USDRenderable {
func radiusString(_ radius:Double) -> String {
"double radius = \(radius)"
}
//NOTE: Multi-geometry relevant support functions live in an
//extension of the protocol USDRenderable
func forUSDA() -> StringNodeable {
CurlyBraced(opening: "def Xform \"\(self.id)\"", style: .expanded) {
if !self.transformations.isEmpty {
transformStringNode(shape:self)
}
CurlyBraced(opening: "def \(self.shapeName) \"\(self.id.lowercased())\"",
style: .expanded) {
"\(extentString(shape: self))"
if !self.surfaces.isEmpty {
colorString(shape:self)
}
"\(radiusString(self.radius))"
}
}
}
}
The USDFileBuilder
still exists, but it takes on a managerial role of all the supported geometries’ extensions and making the initial call to the layer’s _render()
function.
public func generateString(for stage:some Layer) -> String {
let items = stage._walk(items: [])
let document = Document {
stage._render(context: [generateHeader(defaultPrimID:items[defaultPrimIndex])])
}
return document.render(style: .multilineIndented)
}
Note that generateString
had to collect the names of the prims using a _walk
function in order to pick one to be the default:
extension Layer {
public func _walk(items:[String]) -> [String] {
if let bottom = self as? _RenderableLayer {
return bottom.ids(items: items)
} else {
return body._walk(items: items)
}
}
}
extension _RenderableLayer {
func ids(items:[String]) -> [String] {
items + ["\(id)"]
}
}
The X3DFileBuilder
converted over nicely as well. Since I had given the “built in” _render()
and render()
to the USDFileBuilder
, Layer
and each of the scaffolding structs needed new X3D dedicated functions.
protocol _X3DRenderable:_RenderableLayer {
func renderX3D(context:RenderContext) -> RenderContext
}
extension Layer {
func _renderX3D(context:RenderContext) -> RenderContext {
if let bottom = self as? _X3DRenderable {
return bottom.renderX3D(context: context)
} else {
return body._renderX3D(context: context)
}
}
}
extension _ArrayLayer:_X3DRenderable {
func renderX3D(context:RenderContext) -> RenderContext {
var myContext = context
for element in elements {
myContext = element._renderX3D(context: myContext)
}
return myContext
}
}
extension _TupleLayer:_X3DRenderable {
func renderX3D(context:RenderContext) -> RenderContext {
second._renderX3D(context: first._renderX3D(context: context))
}
}
extension _Either: _X3DRenderable {
func renderX3D(context:RenderContext) -> RenderContext {
switch storage {
case .first(let storedLayer):
return storedLayer._renderX3D(context: context)
case .second(let storedLayer):
return storedLayer._renderX3D(context: context)
}
}
}
Then Sphere needs the X3D functions as well.
extension Sphere:_X3DRenderable {
func renderX3D(context: RenderContext) -> RenderContext {
context + [self.forX3D()]
}
}
extension Sphere:X3DRenderable {
func forX3D() -> StringNodeable {
var content = Tag("Shape") {
Tag("Appearance") {
if !self.surfaces.isEmpty {
materialString(self.surfaces)
}
}
"<Sphere radius='\(self.radius)'></Sphere>"
}
if !self.transformations.isEmpty {
let orderedTransforms = self.transformations.reversed()
for item in orderedTransforms {
let attributes = transformAttribute(transform: item)
content = Tag("Transform", attributes:attributes) { content }
}
}
return content
}
}
Update the generateString
function for the X3DFileBuilder
went smoothly as well.
public func generateString(for stage:some Layer) -> String {
let content = stage._renderX3D(context: [])
let document = Document {
TopMatter
Tag("X3D", attributes: X3DAttributes) {
head()
Tag("Scene") {
content
}
}
}
return document.render(style: .multilineIndented)
}
With a quick
swift run sketchpad multiball -s
usdrecord --complexity veryhigh multiball_20230805T084605.usda multiball.png
et voilà:
A high quality render of a multiball output like never before seen!
Also still in X3D: