iOS visual-polish pipeline¶
Intent: "My iOS SceneView demo looks flat. How do I get the polished, reflective, starfield-wrapped look without writing a custom RealityKit scene?"
Three ingredients turn a flat SceneViewSwift scene into a hand-crafted-looking one, and together they cost almost no extra code:
- An HDR skybox — a 360° image-based light that both lights the scene and paints the background.
- PBR materials — physically-based metallic/roughness surfaces that reflect that HDR. This is what produces chrome-mirror polish.
- Apple AR Quick Look — a one-line
.usdzhand-off to iOS's system AR viewer.
Credit
This pipeline is decoded from Eliott Radcliffe (@radcli14)'s
MIT-licensed twolinks project. The
skybox-renders-as-background fix and true-orbit camera (PR #1215),
the PBR default (#1223) and
the bundled night_sky HDR (#1219)
all shipped in v4.4.0 — this page shows how to use them together.
1. HDR skybox¶
SceneViewSwift loads a .hdr/.exr panorama as a RealityKit EnvironmentResource.
As of v4.4.0 it does two things at once:
- Image-based lighting (IBL) — every PBR material samples the HDR for ambient light and reflections.
- Background — when
showSkyboxistrue, the HDR is also painted behind the scene (content.environment = .skybox(resource)), so the camera looks into the panorama instead of a flat void.
Use a bundled preset¶
SceneView {
GeometryNode.sphere(radius: 0.2, material: .pbr(color: .white, metallic: 0.97, roughness: 0.05))
}
.environment(.nightSky) // dramatic Milky Way starfield, CC0, ships with the demos
.cameraControls(.orbit)
SceneEnvironment.allPresets — .studio, .outdoor, .sunset, .night, .warm,
.autumn, .nightSky — all carry an HDR. .nightSky is the most dramatic and is the
direct counterpart to the twolinks starfield. Iterate allPresets to build an
environment picker and every preset (including future ones) surfaces automatically.
Use your own HDR¶
hdrFileresolves against the app bundle — dropcity.hdrinto the Xcode project.intensityscales the IBL contribution (dim a bright sky so it doesn't blow out skin tones;nightSkyships at0.5for exactly this reason).showSkybox: falsekeeps the HDR as a light source only and leaves the background transparent — the correct choice for AR, where the camera feed is the background.
Where to source HDRs
Poly Haven and ambientCG
both publish CC0 (public-domain) HDRIs in 1K–16K. The bundled night_sky
preset is Poly Haven's dikhololo_night
by Greg Zaal. Pick a resolution per platform — 2K is plenty for a phone.
2. PBR materials¶
A SimpleMaterial is effectively unlit-flat: it does not react to the HDR, so even a
4K skybox leaves it looking like dull plastic. A PhysicallyBasedMaterial reflects the
environment — that is the chrome polish.
As of v4.4.0 every procedural shape already defaults to PBR — GeometryNode.cube(color:),
.sphere(color:), ShapeNode, LineNode, lit ImageNode — so the dull-plastic look
is gone by default. You only reach for the explicit material API to tune metallic and
roughness:
// Mirror-chrome — high metallic, near-zero roughness
GeometryNode.sphere(radius: 0.2, material: .pbr(color: UIColor(white: 0.75, alpha: 1),
metallic: 0.97, roughness: 0.05))
// Satin-painted metal — half metallic, soft highlights
GeometryNode.cube(size: 0.2, material: .pbr(color: .systemBlue,
metallic: 0.5, roughness: 0.4))
| Look | metallic |
roughness |
|---|---|---|
| Mirror chrome | 0.97 |
0.05 |
| Brushed metal | 0.9 |
0.4 |
| Satin paint | 0.5 |
0.4 |
| Matte plastic (library default) | 0.0 |
0.5 |
For textured surfaces use .textured(baseColor:normal:metallic:roughness:tint:).
To opt back into the flat look — debug overlays, unlit gizmos — pass unlit: true,
e.g. GeometryNode.cube(color: .red, unlit: true).
PBR needs an HDR to reflect
A metallic material in a scene with no environment reflects nothing and looks black. PBR and the HDR skybox are a package deal — always pair them.
3. Apple AR Quick Look¶
For a .usdz model, you do not need to build an AR scene to let the user place it in
their room. Hand the file to iOS's system AR viewer with QuickLook:
import QuickLook
// In a SwiftUI view, present QLPreviewController over your scene:
.quickLookPreview($arPreviewURL) // arPreviewURL: URL? bound to the bundled .usdz
// Resolve the bundled USDZ and present it:
if let url = Bundle.main.url(forResource: "car", withExtension: "usdz") {
arPreviewURL = url // Quick Look opens; its built-in "AR" button (top-right)
// drops the model into the user's environment.
}
This is the pattern the Model Viewer screen in samples/ios-demo uses: a "View in
AR" button appears whenever the displayed model has a bundled .usdz, and Quick Look's
own AR button does the placement. No ARSceneView, no anchor management.
Planet props
twolinks's earth/moon/sun planets are Apple's free
AR Quick Look "Sky Objects" gallery
USDZ assets — fine to ship inside a demo app, not redistributable as a standalone
library. They are good ready-made PBR props to drop into a night_sky scene.
Putting it together¶
import SwiftUI
import QuickLook
import SceneViewSwift
struct PolishedScene: View {
@State private var arURL: URL?
var body: some View {
ZStack {
SceneView { root in
let chrome = GeometryNode.sphere(
radius: 0.25,
material: .pbr(color: UIColor(white: 0.75, alpha: 1),
metallic: 0.97, roughness: 0.05))
chrome.entity.position = .init(x: 0, y: 0, z: -1.5)
root.addChild(chrome.entity)
}
.environment(.nightSky) // HDR lights + wraps the scene
.cameraControls(.orbit) // true-orbit camera (v4.4.0)
.ignoresSafeArea()
VStack {
Spacer()
Button("View in AR") {
arURL = Bundle.main.url(forResource: "moon", withExtension: "usdz")
}
}
}
.quickLookPreview($arURL) // Apple AR Quick Look hand-off
}
}
That is the entire twolinks look: a chrome sphere under a starfield with a one-tap
AR entry point — no custom RealityKit scene, no hand-tuned lighting rig.
See also¶
- Blender → SceneView asset pipeline — author your own PBR
.usdzprops. - API Cheatsheet (Apple) — full
SceneEnvironmentandGeometryMaterialreference. - Dynamic Sky guide — time-of-day environment switching.
- Migration Guide — the v4.4.0
showSkyboxbackground-render change.
This recipe is decoded from @radcli14's
twolinks project (MIT licensed). Thanks to
Eliott Radcliffe for the reference implementation.