前言
我实现了一个SDL3的KMP绑定:sdl-kmp。之后又基于这个绑定写了一个软光追渲染器:KotlinNativeFortuneRenderer。
其中软光追部分是基于这个教学视频合集实现的,我主要是使用Kotlin Multiplatform重新实现了一遍,并且接入了自己写的sdl-kmp作为窗口和像素显示层。
效果如下:

sdl-kmp
sdl-kmp是一个SDL3的Kotlin Multiplatform绑定。它的目标不是把SDL3的所有底层细节都暴露出来,而是在commonMain中提供一套比较好用的公共API,让窗口创建、事件处理、渲染器、纹理、颜色、矩形这些常用能力可以直接跨平台使用。
它的底层实现分成两部分:
JVM平台使用LWJGL提供的SDL3绑定。Kotlin/Native平台使用cinterop调用SDL3,并且会为不同平台编译SDL3静态库。
Native这边比较重要的一点是,SDL3静态库会被嵌入到发布的klib中,所以使用者不需要再手动准备SDL3动态库。对使用者来说,只要添加依赖,就可以在commonMain中直接使用。
目前支持的平台包括JVM、macOS、Linux、Windows、iOS和tvOS。不支持Android,因为LWJGL本身不支持Android,JS/WASM也不在这个项目的范围内。
使用方式如下:
kotlin
implementation("cn.enaium.sdl:sdl-kmp:1.0.2")
之后就可以使用SDL.init、SDL.createWindow、SDL.createRenderer、SDL.pollEvent这些接口。项目中还提供了一个简单的example模块,用来测试窗口、事件和基本绘制。
比如创建一个什么都不画的空窗口,代码大概是这样:
kotlin
import cn.enaium.sdl.*
fun main() {
SDL.setMainReady()
if (!SDL.init(SDLInitFlags.VIDEO)) {
error("SDL_Init failed: ${SDL.error()}")
}
SDL.createWindow("hello sdl-kmp", 800, 600).use { window ->
SDL.createRenderer(window).use { renderer ->
var running = true
while (running) {
while (true) {
val event = SDL.pollEvent() ?: break
when (event) {
is SDLEvent.Quit -> running = false
is SDLEvent.Key -> {
if (event.down && event.keycode == SDLKeycode.ESCAPE) {
running = false
}
}
else -> Unit
}
}
renderer.drawColor = SDLColor(18, 18, 24)
renderer.clear()
renderer.present()
SDL.delay(16)
}
}
}
SDL.quit()
}
这里的流程基本就是初始化SDL,创建窗口和渲染器,循环读取事件,然后每帧清空窗口并提交。use用于自动释放窗口和渲染器资源,按关闭按钮或者ESC时退出循环。
Fortune Renderer
KotlinNativeFortuneRenderer是我用sdl-kmp写的一个软光追渲染器。这个项目本身也是KMP项目,支持JVM、macosArm64和linuxX64。
它的commonMain主要用了这些依赖:
kotlin
commonMain {
dependencies {
implementation("cn.enaium.sdl:sdl-kmp:1.0.2")
implementation("cn.enaium.glm:glm-kmp:1.0.0")
implementation("io.github.vinceglb:filekit-core:0.15.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
}
}
其中sdl-kmp负责窗口、事件、纹理和像素显示;glm-kmp负责向量、矩阵这些数学计算;filekit-core用于跨平台读取场景文件;kotlinx-serialization-json用于解析JSON场景配置;kotlinx-coroutines-core则用于渲染任务和异步流程的组织。
这个项目的主要目的不是做一个完整的商业渲染器,而是验证几件事:
sdl-kmp是否能支撑真实的跨平台图形程序。Kotlin/Native是否可以比较方便地编写本地窗口程序。- 纯软件路径追踪能否在合理的优化下达到可接受的速度。
渲染器最终输出的是一块像素缓冲区,SDL负责把这块像素数据更新到纹理中并显示到窗口。这样渲染部分完全不依赖GPU光栅化管线,所有光线求交、材质计算、采样和递归追踪都在CPU中完成。
渲染能力
目前渲染器已经支持基本的路径追踪流程,包括多重采样、直接光照、阴影、间接光和金属反射。
几何体方面支持:
- 球体
- 三角形
- 圆盘
材质方面支持:
Lambert漫反射材质Conductor金属材质
灯光方面支持:
- 方向光
- 点光源
- 聚光灯
场景使用JSON配置,可以在配置文件中设置相机、物体、材质和灯光。这样调整场景时不需要重新修改代码,只需要改配置文件即可。
性能
软光追的计算量比较大,如果单线程逐像素计算会非常慢。所以这个项目中使用了多线程渲染,每个 worker 从一个原子计数器中取像素下标,然后计算对应像素。
这样做的好处是很直接:
- 每个像素之间基本没有依赖,适合并行。
- 不需要复杂的任务调度系统。
- 已经渲染出来的像素可以立刻显示到窗口中。
窗口显示部分使用SDL的流式纹理,每帧把当前像素缓冲区更新到纹理中。也就是说渲染不是等整张图片完成后才显示,而是可以看到画面逐渐收敛。
项目还支持headless模式,在没有显示环境的服务器或者CI中,可以通过SDL_VIDEO_DRIVER=dummy运行。
总结
这次主要做了两个项目。第一个是sdl-kmp,用于让Kotlin Multiplatform项目可以方便地调用SDL3。第二个是KotlinNativeFortuneRenderer,它基于sdl-kmp实现了一个跨平台软光追渲染器。
整体做下来,KMP写这类跨平台本地程序是可行的。SDL负责窗口和输入输出,Kotlin负责上层逻辑和渲染算法,Kotlin/Native负责生成本地程序。之后如果继续优化,可以加入BVH、分块调度、更多材质和更好的采样策略。
项目地址: