这是一个旨在用于生成光线追踪场景的库。我最初用 Clojure 实现了它,但我想通过用 Swift 重新实现它来更多地了解 Swift。它是一个库,而不是像 POV-Ray 这样的应用程序,但它包含一个组件,允许您通过在 Swift DSL 中表达来轻松创建对象场景,然后将其渲染到文件。此外,由于您可以使用 Xcode 输入场景,因此您可以利用其自身的功能,如类型检查和 fixits,这在使用 Clojure 时是不可能的。我们实际上是将 Xcode 用作运行一组草图的 GUI。与 Clojure 实现一样,这个实现也是基于 Jamis Buck 撰写的精彩书籍《The Ray Tracer Challenge》中提供的测试。
import ScintillaLib
@main
struct QuickStart: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 2, -2),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
Sphere()
.material(.solidColor(1, 0, 0))
}
}
QuickStart.png
Scintilla 允许您使用光源、相机和一系列形状(每个形状都有相关的材质)来描述和渲染场景。然后可以使用构造实体几何将形状彼此组合。以下是关于这些特性的讨论。
以下基本形状可用
形状 | 默认值 |
---|---|
平面 | 位于 xz 平面中 |
球体 | 以原点为中心,半径为一单位 |
立方体 | 以原点为中心,"半径"为一单位 |
圆锥体 | 以原点为中心,半径为一单位,沿 y 轴方向无限长,并且有外露的盖子 |
圆柱体 | 以原点为中心,半径为一单位,沿 y 轴方向无限长,并且有外露的盖子 |
圆环体 | 以原点为中心,位于 xz 平面中,主半径为 2,次半径为 1 |
目前,所有形状都必须至少使用 Material
构建,其详细信息将在下面解释。
所有形状还具有以下属性修饰符,用于设置/更新底层变换矩阵
translate(_ x: Double, _ y: Double, _ z: Double)
scale(_ x: Double, _ y: Double, _ z: Double)
rotateX(_ t: Double)
rotateY(_ t: Double)
rotateZ(_ t: Double)
shear(_ xy: Double, _ xz: Double, _ yx: Double, _ yz: Double, _ zx: Double, _ zy: Double)
这意味着您可以以逻辑方式将操作链接在一起,而无需显式地 let
出变换矩阵,然后将其传递给形状的构造函数,如下所示
Cube()
.shear(1, 1, 0, 1, 0, 0)
.scale(1, 2, 3)
.rotateX(PI/3)
.rotateY(PI/3)
.rotateZ(PI/3)
.translate(0, 1, 2)
该实现按相反的顺序应用底层变换矩阵,因此程序员不会为此而烦恼,而是可以简单地以直观的方式链接操作。
超椭球体是一系列具有广泛形状多样性的表面,由以下等式中的两个参数 e
和 n
决定
(|x|2/e + |y|2/e)e/n + z2/n = 1
下面是一个超椭球体阵列的渲染,每个超椭球体具有 e
和 n
值的不同组合
import ScintillaLib
@main
struct SuperellipsoidScene: ScintillaApp {
var world: World = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 0, -12),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(0, 5, -5))
for (i, e) in [0.25, 0.5, 1.0, 2.0, 2.5].enumerated() {
for (j, n) in [0.25, 0.5, 1.0, 2.0, 2.5].enumerated() {
Superellipsoid(e: e, n: n)
.material(.solidColor((Double(i)+1.0)/5.0, (Double(j)+1.0)/5.0, 0.2))
.translate(2.5*(Double(i)-2.0), 2.5*(Double(j)-2.0), 0.0)
}
}
}
}
它们的使用方式与任何基本形状完全相同。
隐式曲面实际上是 Shape
的一个子类,但它们的使用方式与其他类型略有不同。隐式曲面是用材质和一个闭包创建的,该闭包表示等式中的函数 F,该等式根据三个坐标定义了曲面,即
F(x, y, z) = 0
由于无法计算 F 的任意选择的边界,因此需要以某种方式将这些边界通知 Scintilla。您可以通过传递一对 3 元组来指定它们,这些 3 元组表示边界框的左下前方角和右上后方角。如果您不这样做,Scintilla 默认为由 (-1, -1, -1) 和 (1, 1, 1) 定义的边界框。以下是使用显式边界框渲染隐式曲面的示例代码,对于以下等式
x² + y² + z² + sin(4x) + sin(4y) + sin(4z) = 1
import Darwin
import ScintillaLib
@main
struct MyWorld: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 0, -5),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
ImplicitSurface(bottomFrontLeft: (-2, -2, -2),
topBackRight: (2, 2, 2), { x, y, z in
x*x + y*y + z*z + sin(4*x) + sin(4*y) + sin(4*z) - 1
})
.material(.solidColor(0.2, 1, 0.5))
}
}
... 这是它的样子
您还可以通过传递一个 3 元组来指定一个边界球,该 3 元组表示球体的中心,以及一个表示其半径的单个双精度值。这对于具有球对称性的隐式曲面(例如下面的 Barth sextic)非常有用。(φ 是黄金比例,1.61833987...)
4(φ²x² - y²)(φ²y² - z²)(φ²z² - x²) - (1 + 2φ)(x² + y² + z² - 1)² = 0
import Darwin
import ScintillaLib
let φ: Double = 1.61833987
@main
struct MyImplicitSurface: ScintillaApp {
var world: World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 0, -5),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-5, 5, -5))
ImplicitSurface(center: (0.0, 0.0, 0.0),
radius: 2.0) { x, y, z in
4.0*(φ*φ*x*x-y*y)*(φ*φ*y*y-z*z)*(φ*φ*z*z-x*x) - (1.0+2.0*φ)*(x*x+y*y+z*z-1.0)*(x*x+y*y+z*z-1.0)
}
.material(.solidColor(0.9, 0.9, 0.0))
}
}
隐式曲面可以像任何其他基本形状一样使用;可以对其进行平移、缩放和旋转,并且其所有材质属性的工作方式也相同。
参数曲面也是 Shape
的一个子类,并且它们的使用方式也与其他原语略有不同。与隐式曲面一样,您需要指定一个边界框,其中包含两个 3 元组,分别表示左下前方角和右上后方角。同样,如果未指定,则边界框默认为由 (-1, -1, -1) 和 (1, 1, 1) 形成的立方体。
但是,与隐式曲面不同,参数曲面用三个闭包表示,这些闭包采用两个参数,一个用于每个坐标。例如,像沙漏一样的曲面由以下参数函数定义
Fx(u, v) = cos(u)sin(2v)
Fy(u, v) = sin(v)
Fz(u, v) = sin(u)sin(2v)
... 这可以在 Scintilla 中表示为以下形式
import Darwin
import ScintillaLib
@main
struct Hourglass: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 1, -5),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
ParametricSurface(bottomFrontLeft: (-1.0, -1.0, -1.0),
topBackRight: (1.0, 1.0, 1.0),
uRange: (0, 2*PI),
vRange: (0, 2*PI),
fx: { (u, v) in cos(u)*sin(2*v) },
fy: { (u, v) in sin(v) },
fz: { (u, v) in sin(u)*sin(2*v) })
.material(.solidColor(0.9, 0.5, 0.5, .hsl))
Plane()
.material(.solidColor(1, 1, 1))
.translate(0, -1.0, 0)
}
}
... 这会导致如下所示的图像
还有两个其他参数可以调整以提高参数曲面的图像质量:精度和最大梯度。它们在 u
和 v
值的范围之后,以及 x
、y
和 z
的三个闭包之前传递。这两个值也都有默认值——精度为 0.001,最大梯度为 1.0——因此您不必总是指定它们。但是,有时某些参数曲面的结果图像可能太锯齿状,因此您可能需要降低精度参数。这是一个有些人为的例子,但在下面我们将精度显着增加(0.1),而不是默认值...
import Darwin
import ScintillaLib
@main
struct Hourglass: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 1, -5),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
ParametricSurface(bottomFrontLeft: (-1.0, -1.0, -1.0),
topBackRight: (1.0, 1.0, 1.0),
uRange: (0, 2*PI),
vRange: (0, 2*PI),
accuracy: 0.1,
maxGradient: 1.0,
fx: { (u, v) in cos(u)*sin(2*v) },
fy: { (u, v) in sin(v) },
fz: { (u, v) in sin(u)*sin(2*v) })
.material(.solidColor(0.9, 0.5, 0.5, .hsl))
Plane()
.material(.solidColor(1, 1, 1))
.translate(0, -1.0, 0)
}
}
... 我们可以清楚地看到它导致形状看起来非常块状
请记住一件事:精度值越小,意味着更高的渲染质量以及更长的渲染时间。
类似地,您可能需要覆盖最大梯度的默认值,以提高某些参数曲面的保真度。一般来说,最大梯度会影响 Scintilla 处理形状弯曲的方式;对于不太弯曲的形状,您可以使用较低的最大梯度值,但对于更弯曲的形状,使用太低的值可能会导致某些形状的部分“掉落”。
(具体来说,最大梯度是参数函数在每个点的所有偏导数的最大值的估计,即 ∂Fx/∂u、∂Fx/∂v、∂Fy/∂u、∂Fy/∂v、∂Fz/∂u 和 ∂Fz/∂v。它用于计算 u
和 v
给定值范围内每个坐标的最小值和最大值,最终在通过相机到形状的每条光线搜索交点时完成。如果您不确定如何选择最佳值,则应从默认值 (1.0) 开始,并不断尝试通过升高或降低它来找到不产生不需要的伪像的最低值。)
例如,在下面,我们从上面取了我们的形状,并将最大梯度设置为 0.3
import Darwin
import ScintillaLib
@main
struct Hourglass: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 1, -5),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
ParametricSurface(bottomFrontLeft: (-1.0, -1.0, -1.0),
topBackRight: (1.0, 1.0, 1.0),
uRange: (0, 2*PI),
vRange: (0, 2*PI),
accuracy: 0.001,
maxGradient: 0.3,
fx: { (u, v) in cos(u)*sin(2*v) },
fy: { (u, v) in sin(v) },
fz: { (u, v) in sin(u)*sin(2*v) })
.material(.solidColor(0.9, 0.5, 0.5, .hsl))
Plane()
.material(.solidColor(1, 1, 1))
.translate(0, -1.0, 0)
}
}
... 我们可以看到我们选择了一个太小的值,并且看到我们缺少形状的重要部分
应该注意的是,即使使用精度和最大梯度的默认值,参数曲面的渲染时间也可能比隐式曲面长得多。但是,有很多曲面不可能(或不容易)用隐式函数表示,因此使用参数曲面可以开辟许多可能性。
Scintilla 中提供的另一种 Shape
类型是 Prism
对象。要使用棱柱,您需要将三个参数传递给构造函数
Double
元组数组,表示多边形顶点的 (x, z) 坐标形状沿 y 轴从基础 y 值挤压到顶部 y 值。这是一个基于星形的棱柱的示例
import ScintillaLib
@main
struct PrismScene: ScintillaApp {
var world: World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 5, -5),
to: Point(0, 1, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-5, 5, -5))
Prism(bottomY: 0.0,
topY: 2.0,
xzPoints: [(1.0, 0.0),
(1.5, 0.5),
(0.5, 0.5),
(0.0, 1.0),
(-0.5, 0.5),
(-1.5, 0.5),
(-1.0, 0.0),
(-1.0, -1.0),
(0.0, -0.5),
(1.0, -1.0)])
.material(.solidColor(1, 0.5, 0))
Plane()
.material(.solidColor(1, 1, 1))
}
}
目前,仅支持连接顶点的线段;也许将来可以支持 Beziér 曲线。尽管如此,凸和凹多边形都得到完全支持。
Scintilla 还提供了一种旋转曲面形状。它最多采用两个参数
Double
元组数组,表示要绕 y 轴旋转的曲线顶点的 (y, z) 坐标false
在渲染时,Scintilla 会计算一条连接各个顶点的分段连续三次样条函数,并有效地将该曲线绕 y 轴旋转。这种形状非常适合创建花瓶或其他弯曲物体,如下所示。
import ScintillaLib
@main
struct SorScene: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 7, -10),
to: Point(0, 2, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-5, 5, -5))
SurfaceOfRevolution(yzPoints: [(0.0, 2.0),
(1.0, 2.0),
(2.0, 1.0),
(3.0, 0.5),
(6.0, 0.5)])
.material(.solidColor(0.5, 0.6, 0.8))
Plane()
.material(.solidColor(1, 1, 1))
}
}
截至本文撰写之时,只有三次样条策略可用于插值顶点。
目前,材质采用以下任一颜色方案:
此外,所有材质类型都具有以下属性:
属性 | 取值范围 |
---|---|
环境反射率 | 0.0 - 1.0 |
漫反射率 | 0.0 - 1.0 |
镜面反射率 | 0.0 - 1.0 |
光泽度 | 0.0 - ∞ |
反射指数 | 0.0 - 1.0 |
透明度 | 0.0 - 1.0 |
折射率 | 1.0 - 2.5 |
有一个默认材质 SolidColor.basicMaterial()
,可以方便地使用,它具有上述所有属性的默认值和纯白色着色方案。 与 Shape
一样,Material
具有属性修改器,可用于指定其他属性的值,而无需一次传递所有非默认值
.ambient(_ n: Double)
.diffuse(_ n: Double)
.specular(_ n: Double)
.shininess(_ n: Double)
.reflective(_ n: Double)
.transparency(_ n: Double)
.refractive(_ n: Double)
要将材质与 Shape
关联,您可以调用 .material()
属性修改器并传入 Material
实例。 为了方便起见,提供了三种静态方法来实现此目的:
.solidColor(_ r: Double, _ g: Double, _ b: Double)
.pattern(_ pattern: Pattern)
.colorFunction(_ f: ColorFunctionType)
例如,要为立方体创建 3D 格子图案,您可以编写以下代码:
Cube()
.material(.pattern(Checkered3D(.white, .black, .identity)))
以下图案可在 Scintilla 中使用:
Striped(_ firstColor: Color, _ secondColor: Color, _ transform: Matrix4)
Checkered2D(_ firstColor: Color, _ secondColor: Color, _ transform: Matrix4)
Checkered3D(_ firstColor: Color, _ secondColor: Color, _ transform: Matrix4)
Gradient(_ firstColor: Color, _ secondColor: Color, _ transform: Matrix4)
或者,要使用颜色函数,您可以这样做:
Cube()
.material(.colorFunction({ x, y, z in
(abs(sin(x)), abs(sin(y)), abs(sin(z)))
}))
颜色可以用 RGB 和 HSL 颜色空间表示。 默认情况下,颜色是在 RGB 颜色空间中构建的; 如果要为具有纯色的材质使用 HSL 空间,您可以像下面这样传入 .hsl
枚举案例:
Sphere()
.translate(0, 1, 0)
.material(.solidColor(0.5, 1.0, 0.5, .hsl))
您也可以为使用颜色函数的材质使用 HSL 颜色空间,如下所示:
Sphere()
.material(.colorFunction(.hsl) { x, y, z in
((atan(z/x)+PI/2.0)/PI, 1.0, 0.5)
})
支持以下三种操作来组合各种形状:
CSG 的实现利用了所谓的result builders,这是 Swift 的一项功能,允许程序员以最少的标点符号列出函数的参数。 此外,Scintilla 负责嵌套 CSG 操作对,因此您不必这样做,因此您可以像这样表达从球体中减去三个圆柱体:
Sphere()
.material(.solidColor(Color(0, 0, 1)))
.difference {
Cylinder()
.material(.solidColor(0, 1, 0))
.scale(0.6, 0.6, 0.6)
Cylinder()
.material(.solidColor(0, 1, 0))
.scale(0.6, 0.6, 0.6)
.rotateZ(PI/2)
Cylinder()
.material(.solidColor(0, 1, 0))
.scale(0.6, 0.6, 0.6)
.rotateX(PI/2)
}
... 而不必这样做:
CSG(.difference,
CSG(.difference,
CSG(.difference,
Sphere()
.material(.solidColor(0, 0, 1))),
Cylinder()
.material(.solidColor(0, 1, 0))
.scale(0.5, 0.5, 0.5)),
Cylinder()
.material(.solidColor(0, 1, 0))
.scale(0.5, 0.5, 0.5)
.rotateZ(PI/2)),
Cylinder()
.material(.solidColor(0, 1, 0))
.scale(0.5, 0.5, 0.5)
.rotateX(PI/2))
您甚至可以在表达式的中间使用 for
循环来完成相同的操作:
Sphere()
.material(.solidColor(0, 0, 1))
.difference {
for (thetaX, thetaZ) in [(0, 0), (0, PI/2), (PI/2, 0)] {
Cylinder()
.material(.solidColor(0, 1, 0))
.scale(0.6, 0.6, 0.6)
.rotateX(thetaX)
.rotateZ(thetaZ)
}
}
您还可以链接对 .union()
、.intersection()
和 .difference()
的调用来创建复杂的形状:
Sphere()
.material(.solidColor(0, 0, 1))
.intersection {
Cube()
.material(.solidColor(1, 0, 0))
.scale(0.8, 0.8, 0.8)
}
.difference {
for (thetaX, thetaZ) in [(0, 0), (0, PI/2), (PI/2, 0)] {
Cylinder()
.material(.solidColor(0, 1, 0))
.scale(0.5, 0.5, 0.5)
.rotateX(thetaX)
.rotateZ(thetaZ)
}
}
有时您不一定想像上面那样组合形状来制作新形状; 有时您只想能够将它们组合在一起,以便它们可以一起移动或以其他方式变换。 例如,如果您想将两个球体绕 z 轴相互旋转,您可以这样做:
import ScintillaLib
@available(macOS 12.0, *)
@main
struct QuickStart: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 0, -5),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
Sphere()
.material(.solidColor(1, 0, 0))
.translate(-1, 0, 0)
.rotateZ(PI/2)
Sphere()
.material(.solidColor(0, 1, 0))
.translate(1, 0, 0)
.rotateZ(PI/2)
}
}
请注意,我们必须应用两次相同的旋转。 我们可以通过将两个球体放在一个组中并旋转该组来做得更好:
import ScintillaLib
@available(macOS 12.0, *)
@main
struct QuickStart: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 0, -5),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
Group {
Sphere()
.material(.solidColor(1, 0, 0))
.translate(-1, 0, 0)
Sphere()
.material(.solidColor(0, 1, 0))
.translate(1, 0, 0)
}
.rotateZ(PI/2)
}
}
在这个例子中,它并不是一个巨大的收获,但是如果您正在构建包含更多对象的场景,您可以节省 *很多* 代码重复。 即使在下面的示例中,您也可以看到节省,因为您不必单独创建和转换四个球体; 您只需将其中两个球体组合在一起,然后创建平移该组的两个副本:
import ScintillaLib
@available(macOS 12.0, *)
@main
struct QuickStart: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 0, -5),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
for x in [-1.5, 1.5] {
Group {
Sphere()
.material(.solidColor(1, 0, 0))
.translate(-1, 0, 0)
Sphere()
.material(.solidColor(0, 1, 0))
.translate(1, 0, 0)
}
.rotateZ(PI/2)
.translate(x, 0, 0)
}
}
}
请注意,组也可以利用result builders,如您在上面看到的,您可以一次列出一个组的多个对象,而不是嵌套成对对象的组。
Scintilla 目前支持两种 Light
:PointLight
和 AreaLight
。 PointLight
至少需要构造一个位置,如果在未指定其他颜色,则默认为白色。 光线从一个点(PointLight
的位置)发出,并投射到世界上。
AreaLight
需要更多信息才能构造:
参数名称 | 描述 |
---|---|
角 |
一个 Tuple4 对象,表示光源角的 x、y 和 z 坐标 |
颜色 |
光源的 Color |
fullUVec |
一个 Tuple4 对象,表示光源一个维度的方向和大小 |
uSteps |
沿 fullUVec 定义的向量的细分数量 |
fullVVec |
一个 Tuple4 对象,表示光源第二个维度的方向和大小 |
vSteps |
沿 fullVVec 定义的向量的细分数量 |
以下图表可能更清楚地理解这些参数代表什么:
--------- fullVVec ------->
^ |------|------|------|------|
| | | * | | * |
| | * | | * | |
| |------|------|------|------|
| | | | * | *|
fullUVec | * | * | | |
| |------|------|------|------|
| | | * | * | |
| | * | | |* |
| |------|------|------|------|
corner
AreaLight
可以被认为是一个矩形,由多个单元格组成,数量为 uSteps
*vSteps
,而不是单个点光源。 对于要渲染的场景中的每个像素,都会从每个单元格投射一条光线,这些单元格的位置从每个单元格的中心随机“抖动”,如上面的星号所示。 然后将与每条光线相关的颜色进行平均并分配给场景中的每个像素,其主要结果是对象阴影更柔和。 您可以在下面看到明显的差异。
用点光源渲染的场景
使用面积光在与上述点光源相同的位置渲染的场景,但在两个维度上具有 10 个细分
import ScintillaLib
@main
struct MyWorld: ScintillaApp {
var world: World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 2, -5),
to: Point(0, 1, 0),
up: Vector(0, 1, 0))
AreaLight(corner: Point(-5, 5, -5),
uVec: Vector(2, 0, 0),
uSteps: 10,
vVec: Vector(0, 2, 0),
vSteps: 10)
Sphere()
.translate(0, 1, 0)
.material(.solidColor(1, 0, 0))
Plane()
.material(.solidColor(1, 1, 1))
}
}
请注意:使用 AreaLight
会导致更长的渲染时间,该时间与 uSteps
和 vSteps
参数的值成正比。
您还可以拥有多个灯光,您可以使用它们来创建具有多个阴影和/或为形状提供更多高光的场景。 下面是一个面条状形状,有两个光源,一个在左边,一个在右边:
import Darwin
import ScintillaLib
@available(macOS 12.0, *)
@main
struct Cavatappi: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 7, -15),
to: Point(0, 7, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
PointLight(position: Point(10, 10, -10))
ParametricSurface(bottomFrontLeft: (-3.5, 0, -3.5),
topBackRight: (3.5, 15.0, 3.5),
uRange: (0, 2*PI),
vRange: (0, 7*PI),
accuracy: 0.001,
maxGradient: 1.0,
fx: { (u, v) in (2 + cos(u) + 0.1*cos(8*u))*cos(v) },
fy: { (u, v) in 2 + sin(u) + 0.1*sin(8*u) + 0.5*v },
fz: { (u, v) in (2 + cos(u) + 0.1*cos(8*u))*sin(v) })
.material(.solidColor(1.0, 0.8, 0))
Plane()
.material(.solidColor(1, 1, 1))
.translate(0, -3.0, 0)
}
}
... 结果非常引人注目:
灯光也可以配置为随着到场景中物体的距离的推移而衰减。 您可以使用可选的 fadeDistance
参数来控制光强度的衰减程度; 较大的值意味着光需要更多的距离才能衰减,较小的值会导致更急剧的下降。
import ScintillaLib
@available(macOS 12.0, *)
@main
struct DimlyLitScene: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 0, -5),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, 0),
fadeDistance: 10)
Sphere()
.material(.solidColor(1, 0.5, 0))
Plane()
.translate(0, -1, 0)
}
}
与参数曲面的精度和最大梯度参数一样,您可能需要试验各种值才能获得所需的效果。
要构建场景,您需要创建一个包含以下对象的 World
实例:
Camera
Light
Shape
灯光和形状在上面讨论过。 Camera
接受以下四个参数:
World
还支持使用result builders枚举形状,因此您可以执行以下操作:
World {
Camera(width: 800,
height: 600,
viewAngle: PI/3,
from: Point(0, 3, -5),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
Sphere()
.material(.solidColor(1, 0, 0))
.translate(-2, 0, 0)
Sphere()
.material(.solidColor(0, 1, 0))
Sphere()
.material(.solidColor(0, 0, 1))
.translate(2, 0, 0)
请注意,World
构造函数的参数之间缺少逗号,并且 Sphere
对象周围不需要括号。
Scintilla 提供了一个组件,使您可以轻松地创建应用程序并渲染场景。 为此,首先使用“命令行工具”模板创建一个新的 Xcode 项目。
接下来,通过“文件”->“添加包”将 Scintilla 添加为包依赖项; 在该对话框中,输入此 Git 存储库的 URL,然后单击“添加包”。 Xcode 应该成功下载该库并将其添加到项目中。
现在您已准备好使用 Scintilla,您需要做的就是创建一个新的 Swift 文件,例如 MyWorld.swift
。 添加以下代码作为示例场景:
import ScintillaLib
@main
struct MyWorld: ScintillaApp {
var world = World {
Camera(width: 800,
height: 600,
viewAngle: PI/3,
from: Point(0, 1, -2),
to: Point(0, 0, 0),
up: Vector(0, 1, 0))
PointLight(position: Point(-10, 10, -10))
Sphere()
.material(.solidColor(0, 0, 1))
.intersection {
Cube()
.material(.solidColor(1, 0, 0))
.scale(0.8, 0.8, 0.8)
}
.difference {
for (thetaX, thetaZ) in [(0, 0), (0, PI/2), (PI/2, 0)] {
Cylinder()
.material(.solidColor(0, 1, 0))
.scale(0.5, 0.5, 0.5)
.rotateX(thetaX)
.rotateZ(thetaZ)
}
}
.rotateY(PI/6)
}
}
请注意以上示例中的以下几点:
import ScintillaLib
@main
注释结构体ScintallaApp
协议world
属性,该属性的类型为 World
如果您完成了所有这些操作,那么您现在拥有一个真正的应用程序,并且应该能够在 Xcode 中运行它。 如果一切顺利,您应该看到一个窗口打开,其中包含渲染的图像,并且在桌面上找到文件 MyWorld.png
。
您还可以选择使用抗锯齿渲染场景。 在上面的图像中,您可以看到物体的各个边缘都非常锯齿状,从而降低了图像的真实感。 通过向 World
对象添加属性修改器 .antialiasing(true)
,我们可以提高其质量:
import Darwin
import ScintillaLib
@available(macOS 12.0, *)
@main
struct Cavatappi: ScintillaApp {
var world = World {
Camera(width: 400,
height: 400,
viewAngle: PI/3,
from: Point(0, 7, -15),
to: Point(0, 7, 0),
up: Vector(0, 1, 0),
antialiasing: true)
PointLight(position: Point(-10, 10, -10))
PointLight(position: Point(10, 10, -10))
ParametricSurface(bottomFrontLeft: (-3.5, 0, -3.5),
topBackRight: (3.5, 15.0, 3.5),
uRange: (0, 2*PI),
vRange: (0, 7*PI),
accuracy: 0.001,
maxGradient: 1.0,
fx: { (u, v) in (2 + cos(u) + 0.1*cos(8*u))*cos(v) },
fy: { (u, v) in 2 + sin(u) + 0.1*sin(8*u) + 0.5*v },
fz: { (u, v) in (2 + cos(u) + 0.1*cos(8*u))*sin(v) })
.material(.solidColor(1.0, 0.8, 0))
Plane()
.material(.solidColor(1, 1, 1))
.translate(0, -3.0, 0)
}
}
... 下面是结果图像:
您应该能够看到它比本 README 中更靠前的原始图像“锯齿”少得多。
由于启用抗锯齿会使渲染时间慢得多,因此您应确保将运行配置设置为“Release”,以便以最快的速度运行 Swift。 要到达那里,请转到“产品”->“方案”->“编辑方案...”
您可以通过“文件”->“新建”->“目标...”在单个项目中拥有多个场景。 只需确保 ScintillaLib 作为库包含在目标中; 转到项目导航器,单击项目名称,然后单击编辑器窗格中的目标名称,然后单击“常规”选项卡。
您将在本存储库的顶层 Examples
文件夹中找到很多示例场景。 克隆并打开此存储库后,您应该能够直接在 Xcode 中运行它们。