Hello USD - Part 6: Same as Part 1... but Swift

Back from vacation and raring… to do what I just did about a week ago! Well, to do it with Swift and some handy validation tools.

Making the File

Back in February I looked into doing scripting in Swift as bare bones as possible (No ArgumentParser, no package).

Building on that, I compiled a multi-file example with swiftc *swift -o multiball in the directory with the following collection of .swift files. (./multiball to run.)

Not a lot of new territory here, these Swift files recreate the approach from the Python file in part one with two structural differences:

main.swift

The code entry point is an explicitly named main.swift file.

NOTE: If there is no file called main.swift, there must be something else telling the compiler where to start. In the APIng package, for example, the struct APIng has the label @main and a function main() This level of control was added in Swift 5.3 (For history see 2014 Apple dev post Files and Initialization)

import Foundation

let minX = -4.0
let maxX = 4.0
let minY = minX
let maxY = maxX
let minZ = minX
let maxZ = maxX
let minRadius = 0.8
let maxRadius = 2.0

func timeStampForFile() -> String {
    //Date.now.ISO8601Format()
    let date = Date.now
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyyMMdd'T'HHmmss"
    return formatter.string(from: date)
}

@StringBuilder func  makeMultiBall(count:Int) -> String {
    let builder = USDAFileBuilder()
    builder.generateHeader(defaultPrim:"blueSphere")
    builder.buildItem("blueSphere", "sphere_base", "sphere", 0, 0, 0, 1, 0, 0, 1)

    for i in (0...count) {
        builder.buildItem(
            "sphere_\(i)", 
            "sphere_base", 
            "sphere", 
            Double.random(in: minX...maxX), 
            Double.random(in: minY...maxY), 
            Double.random(in: minZ...maxZ), 
            Double.random(in: minRadius...maxRadius), 
            Double.random(in: 0...1), 
            Double.random(in: 0...1), 
            Double.random(in: 0...1)
        )
    }
}

let inputArgs = CommandLine.arguments.dropFirst()
print("Number of arguments:", inputArgs.count)

var count:Int = 12
var fileName:String = "multiball_\(timeStampForFile()).usda"

switch (inputArgs.count) {
    case 1:
        guard let tmp_count = Int(inputArgs[0]) else {
            fatalError("Argument is not a number so can't create a count")
        }
        count = tmp_count
    case 2: 
        guard let tmp_count = Int(inputArgs[0]) else {
            fatalError("Argument is not a number so can't create a count")
        }
        fileName = inputArgs[1]
        count = tmp_count
    default:
        print("Undecipherable number of arguments. Using defaults.")
}

print("\(count), \(fileName)")
let usdFileText = makeMultiBall(count: count)
let fileURL = URL(filePath: fileName)

do {
    try usdFileText.write(to: fileURL, atomically: true, encoding: String.Encoding.utf8)
} catch { 
    print(error)
}

StringBuilder.swift

import Foundation

@resultBuilder
struct StringBuilder {
    static func buildBlock(_ parts: String...) -> String {
        parts.joined(separator: "\n")
    }

    static func buildOptional(_ component:String?) -> String {
        component ?? ""
    }

    static func buildEither(first component: String) -> String {
        return component
    }

    static func buildEither(second component: String) -> String {
        return component
    }

    static func buildArray(_ components: [String]) -> String {
        components.joined(separator: "\n")
    }
}

Resources

USDAFileBuilder.swift

struct USDAFileBuilder {
@StringBuilder func generateHeader(defaultPrim:String, metersPerUnit:Double = 1, upAxis:String = "Y", documentationNote:String? = nil) -> String {
    "#usda 1.0\n("
    "\tdefaultPrim = \"\(defaultPrim)\""
    "\tmetersPerUnit = \(metersPerUnit)"
    "\tupAxis = \"\(upAxis)\""
    if let documentationNote {
        "doc = \"\(documentationNote)\""
    }
    ")"
}

func translateString(_ xoffset:Double, _ yoffset:Double, _ zoffset:Double) -> String {
    return "\tdouble3 xformOp:translate = (\(xoffset), \(yoffset), \(zoffset))"
}

func opOrderStringTranslateOnly() -> String {
    "\tuniform token[] xformOpOrder = [\"xformOp:translate\"]"
}
    
func colorString(_ red:Double, _ green:Double, _ blue:Double) -> String {
    "\t\tcolor3f[] primvars:displayColor = [(\(red), \(green), \(blue))]"
}
    
func  radiusString(_ radius:Double) -> String {
     "\t\tdouble radius = \(radius)"
}

@StringBuilder func  buildItem(_ id:String, _ reference_file:String, _ geometry_name:String, _ xoffset:Double, _ yoffset:Double, _ zoffset:Double, _ radius:Double, _ red:Double, _ green:Double, _ blue:Double) -> String {
    """
    \nover "\(id)" (\n\tprepend references = @./\(reference_file).usd@\n)\n{
    """
    
    if xoffset != 0 || yoffset != 0 || zoffset != 0 {
        translateString(xoffset, yoffset, zoffset)
        opOrderStringTranslateOnly()
    }
               
    """
    \tover "\(geometry_name)"\n\t{
    """
    colorString(red, green, blue)
    radiusString(radius)
    "\t}"
    "}"
} 
}

Validating the File

# pwd folder with files to check. 
# shell session launched with .command file / working build in path
usdchecker sphere_base.usd
usdchecker multiball.usda

The Pixar USD library comes with a usdchecker utility that, when pointed at the original output of Part 01’s multiball_20230627T132245.usd file, spits out some errors:

Stage does not specify an upAxis. (fails 'StageMetadataChecker')
Stage does not specify its linear scale in metersPerUnit. (fails 'StageMetadataChecker')
Stage has missing or invalid defaultPrim. (fails 'StageMetadataChecker')
Failed!

The generateHeader function fixes those problems in the new files by adding the needed meta data.

#usda 1.0
(
	defaultPrim = "blueSphere"
	metersPerUnit = 1.0
	upAxis = "Y"
)

Other fixes will be necessary because choosing the origin marker “blueSphere” as the defaultPrim doesn’t exactly fit conceptually with the spheres acting as an assemblage. I’m wondering if creating scene or an assemblage will also fix the USDZ Tools usdARKitChecker failure. This script may or may not jibe with what the latest ARKit imports can do, but I still may try to get a passing check.

# Python3 here referes to 3.9.17, against fresh build 
# of Pixar tools. NOT the included USDZ Tools build
$ python3 /Applications/usdpython/usdzconvert/usdARKitChecker -v multiball_20230709T222354.usda
# If that's verbose...
> usdARKitChecker: [Fail] multiball_20230709T222354.usda

For now though it works!

Full Output File

INSERT DESCRIPTIOn

#usda 1.0
(
	defaultPrim = "blueSphere"
	metersPerUnit = 1.0
	upAxis = "Y"

)

over "blueSphere" (
	prepend references = @./sphere_base.usd@
)
{

	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.0, 0.0, 1.0)]
		double radius = 1.0
	}
}

over "sphere_0" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (-1.184153285968514, -3.647033671109095, -3.7641420584021335)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.16205418618238365, 0.5734559688991189, 0.38217704094058846)]
		double radius = 1.4002723309372764
	}
}

over "sphere_1" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (-0.5261383868331437, 3.132048637184454, -0.44706351337151684)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.7178610937878546, 0.2673176415923212, 0.27736339801237053)]
		double radius = 1.6608855195133203
	}
}

over "sphere_2" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (2.696991757170985, -2.741134838471454, 2.2709444095213973)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.9498042527272043, 0.9570184832609097, 0.7820600053071775)]
		double radius = 1.7846037873381584
	}
}

over "sphere_3" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (-1.0308012065228462, 0.45496116972412803, 2.1602285177573757)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.7291305159904483, 0.07002904075700511, 0.7767305210318993)]
		double radius = 1.697103659651786
	}
}

over "sphere_4" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (1.083654492166584, 3.495198151700426, 3.001139568538912)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.19330187806556043, 0.5521787997886198, 0.6234825293272558)]
		double radius = 1.9267084402944623
	}
}

over "sphere_5" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (-0.7130466964579423, -1.6967393634184456, -2.8650597646849745)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.40969230788788025, 0.3848698971870308, 0.38478111434269124)]
		double radius = 1.5136302027051698
	}
}

over "sphere_6" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (2.114100481739248, -1.2331921314814318, 2.9210878637892135)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.42377170754294957, 0.8944820544786956, 0.8387197785634217)]
		double radius = 0.996100383889179
	}
}

over "sphere_7" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (0.6139729851730387, -2.1636073894804015, -1.2448903303953696)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.7496581061490399, 0.6634379091934232, 0.5005325739057677)]
		double radius = 1.019717277295782
	}
}

over "sphere_8" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (1.8976158439642976, 2.6610769940080488, -2.983000650753242)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.40540766717836174, 0.3829469193133114, 0.8038967224000645)]
		double radius = 1.0486640607196256
	}
}

over "sphere_9" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (1.7544553609781461, 3.6112312528391275, -1.0795222118663084)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.36375574825435375, 0.4873766098052579, 0.2602608790257742)]
		double radius = 0.8077356270763054
	}
}

over "sphere_10" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (-3.1792781878340888, 0.6093081424155065, 2.663220325702283)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.8799012009411763, 0.7636616404114634, 0.661145399339866)]
		double radius = 1.2028816672373872
	}
}

over "sphere_11" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (-2.1455875292572246, -3.464486780123318, 3.4854264040544267)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.5864557819094003, 0.5480884060810113, 0.608442805757727)]
		double radius = 1.5136281018955642
	}
}

over "sphere_12" (
	prepend references = @./sphere_base.usd@
)
{
	double3 xformOp:translate = (-2.720272864042377, 1.1241795502758443, 3.0341149159083036)
	uniform token[] xformOpOrder = ["xformOp:translate"]
	over "sphere"
	{
		color3f[] primvars:displayColor = [(0.9908854203828784, 0.833821856282827, 0.5439826142768127)]
		double radius = 0.9554204991635202
	}
}