Hello USD - Part 6: Same as Part 1... but Swift
- MacOS 13.4 (Intel & Mac)
- VSCode 1.79.2 (Swift plugin from server working group v1.4.0)
- swift-driver version: 1.75.2 Apple Swift version 5.8.1 (swiftlang-5.8.0.124.5 clang-1403.0.22.11.100)
- Pixar USD Library 23.05, (python 3.9.17 PySide 6.4.3)
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:
- the added convenience of a
@resultBuilder
calledStringBuilder
- the string creating functions live in their own strut called
USDFileBuilder
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
- https://www.hackingwithswift.com/swift/5.4/result-builders
- https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md
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
#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
}
}