ShadowArt
A solid carved so that it throws a ring from the front and a cross from the side. The shadow it really throws is drawn beside the shadow that was asked for (shadowArt, shadow(from:)).
- Run
swift run --package-path Examples Example-3D-Geometry-ShadowArt- Source
- Examples/3D/Geometry/ShadowArt/Sketch.swift
import Ollin
/// One solid, two shadows, and neither shadow looks like the solid.
///
/// A lit point casts a shadow along the light's direction, so a point can only be
/// part of the solid if it lands inside the shadow in *every* direction it is lit
/// from. Keep exactly those points and what is left is the largest solid that
/// could cast them.
///
/// Here the front asks for a ring and the side asks for a lopsided cross. The
/// solid that comes out looks like neither, and turning it shows why: it is the
/// two silhouettes swept through each other and cut where they cross.
///
/// The two shadows it really throws are drawn under it, beside the two it was
/// asked for. They match, because these two are solid in the same rows. That is
/// the catch worth knowing: the front and the side are seen from either end of the
/// same vertical axis, so a row that is empty in one empties it in the other. Hold
/// the mouse to ask for a third shadow from above as well, and watch all three
/// fall short of what was asked, since no solid can throw them all.
@main
final class ShadowArt_Example: Sketch {
override var loopDuration: Double? { 18 }
private let resolution = 56
private var front = Image(width: 96, height: 96, color: .black)
private var side = Image(width: 96, height: 96, color: .black)
private var above = Image(width: 96, height: 96, color: .black)
private var carved: ShadowArt?
private var solid = Mesh(positions: [], indices: [])
private var withAbove: ShadowArt?
private var withAboveMesh = Mesh(positions: [], indices: [])
override func setup() {
paint(front) { u, v in
let r = (u * u + v * v).squareRoot()
return r < 0.86 && r > 0.42
}
paint(side) { u, v in
(abs(u) < 0.3 && abs(v) < 0.86) || (abs(v - 0.2) < 0.26 && u < 0.7 && u > -0.86)
}
paint(above) { u, v in abs(u) + abs(v) < 0.8 }
carved = shadowArt(fromFront: front, fromSide: side, resolution: resolution)
solid = carved?.mesh ?? Mesh(positions: [], indices: [])
withAbove = shadowArt(fromFront: front, fromSide: side, fromAbove: above,
resolution: resolution)
withAboveMesh = withAbove?.mesh ?? Mesh(positions: [], indices: [])
}
override func draw() {
background(Color(hex: 0x06080D))
let three = mouseIsPressed
guard let art = three ? withAbove : carved else { return }
lightingPreset(.studio)
cameraShowcase(.autoOrbit(period: 18), radius: 4.4, elevation: 0.3,
fieldOfView: .pi / 3.6)
fill(Color(hex: 0xE8C36A))
specular(0.35)
specularSharpness(40)
drawMesh(three ? withAboveMesh : solid)
// What was asked for, and what is really thrown.
let sides: [(ShadowArt.Side, Image, String)] = three
? [(.front, front, "front"), (.side, side, "side"), (.above, above, "above")]
: [(.front, front, "front"), (.side, side, "side")]
let panel = 150.0
let gap = 28.0
let total = Double(sides.count) * (panel * 2 + gap) + gap * Double(sides.count - 1)
var x = (width - total) / 2
for (which, asked, name) in sides {
drawPanel(asked, at: Vector2(x, height - panel - 90), size: panel,
title: "\(name): asked")
drawCast(art.shadow(from: which), at: Vector2(x + panel + gap, height - panel - 90),
size: panel, title: "\(name): thrown")
x += panel * 2 + gap * 2
}
drawCaption(three
? "three shadows: no solid throws them all, so each one comes up short"
: "two shadows, both thrown exactly; hold the mouse to ask for a third")
}
private func drawPanel(_ picture: Image, at corner: Vector2, size: Double, title: String) {
drawImage(picture, in: Rectangle(x: corner.x, y: corner.y, width: size, height: size),
fit: .cover)
label(title, at: Vector2(corner.x, corner.y + size + 16))
}
private func drawCast(_ flags: [Bool], at corner: Vector2, size: Double, title: String) {
let n = resolution
let cell = size / Double(n)
noStroke()
fill(Color(hex: 0xE8C36A))
for row in 0 ..< n {
for column in 0 ..< n where flags[column + row * n] {
drawRect(Rectangle(x: corner.x + Double(column) * cell,
y: corner.y + Double(row) * cell,
width: cell + 0.5, height: cell + 0.5))
}
}
noFill()
stroke(Color(white: 0.3))
strokeWeight(1)
drawRect(Rectangle(x: corner.x, y: corner.y, width: size, height: size))
label(title, at: Vector2(corner.x, corner.y + size + 16))
}
private func label(_ text: String, at point: Vector2) {
noStroke()
fill(Color(white: 0.62))
textSize(15)
textAlign(.left, .middle)
drawText(text, point.x, point.y)
}
private func paint(_ picture: Image, _ inside: (Double, Double) -> Bool) {
for y in 0 ..< picture.height {
for x in 0 ..< picture.width {
let u = (Double(x) + 0.5) / Double(picture.width) * 2 - 1
let v = (Double(y) + 0.5) / Double(picture.height) * 2 - 1
picture[x, y] = inside(u, v) ? .white : .black
}
}
}
}