目的

这是一个旨在用于生成光线追踪场景的库。我最初用 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))
    }
}

特性

Scintilla 允许您使用光源、相机和一系列形状(每个形状都有相关的材质)来描述和渲染场景。然后可以使用构造实体几何将形状彼此组合。以下是关于这些特性的讨论。

基本形状

以下基本形状可用

形状 默认值
平面 位于 xz 平面中
球体 以原点为中心,半径为一单位
立方体 以原点为中心,"半径"为一单位
圆锥体 以原点为中心,半径为一单位,沿 y 轴方向无限长,并且有外露的盖子
圆柱体 以原点为中心,半径为一单位,沿 y 轴方向无限长,并且有外露的盖子
圆环体 以原点为中心,位于 xz 平面中,主半径为 2,次半径为 1

目前,所有形状都必须至少使用 Material 构建,其详细信息将在下面解释。

所有形状还具有以下属性修饰符,用于设置/更新底层变换矩阵

这意味着您可以以逻辑方式将操作链接在一起,而无需显式地 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)

该实现按相反的顺序应用底层变换矩阵,因此程序员不会为此而烦恼,而是可以简单地以直观的方式链接操作。

超椭球体

超椭球体是一系列具有广泛形状多样性的表面,由以下等式中的两个参数 en 决定

(|x|2/e + |y|2/e)e/n + z2/n = 1

下面是一个超椭球体阵列的渲染,每个超椭球体具有 en 值的不同组合

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)
    }
}

... 这会导致如下所示的图像

还有两个其他参数可以调整以提高参数曲面的图像质量:精度和最大梯度。它们在 uv 值的范围之后,以及 xyz 的三个闭包之前传递。这两个值也都有默认值——精度为 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。它用于计算 uv 给定值范围内每个坐标的最小值和最大值,最终在通过相机到形状的每条光线搜索交点时完成。如果您不确定如何选择最佳值,则应从默认值 (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 对象。要使用棱柱,您需要将三个参数传递给构造函数

形状沿 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 还提供了一种旋转曲面形状。它最多采用两个参数

在渲染时,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 具有属性修改器,可用于指定其他属性的值,而无需一次传递所有非默认值

要将材质与 Shape 关联,您可以调用 .material() 属性修改器并传入 Material 实例。 为了方便起见,提供了三种静态方法来实现此目的:

例如,要为立方体创建 3D 格子图案,您可以编写以下代码:

Cube()
    .material(.pattern(Checkered3D(.white, .black, .identity)))

以下图案可在 Scintilla 中使用:

或者,要使用颜色函数,您可以这样做:

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 目前支持两种 LightPointLightAreaLightPointLight 至少需要构造一个位置,如果在未指定其他颜色,则默认为白色。 光线从一个点(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 会导致更长的渲染时间,该时间与 uStepsvSteps 参数的值成正比。

您还可以拥有多个灯光,您可以使用它们来创建具有多个阴影和/或为形状提供更多高光的场景。 下面是一个面条状形状,有两个光源,一个在左边,一个在右边:

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 接受以下四个参数:

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)
    }
}

请注意以上示例中的以下几点:

如果您完成了所有这些操作,那么您现在拥有一个真正的应用程序,并且应该能够在 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 中运行它们。

相关链接