一个示例 D3D11 程序的全流程记录

本篇是笔者进行 RenderDoc drivers 层分析时一并记录下来的,是一个渲染 Cube 的简单 D3D11 程序与 API 交互的全流程的记录。

DirectX11-With-Windows-SDK - Rendering A Cube 为例进行说明。

DXGI (DirectX Graphics Infrastructure) 负责抽象和交换链、DAL 相关的公共部分。关于 DXGI 的资料可以参考 MSDN

DXGI 封装了多种对象:

  • 显示适配器 (IDXGIAdapter): 一般对应一块显卡,也可以对应 Reference Rasterizer,或者支持虚拟化的显卡的一个 VF 等
  • 显示输出 (IDXGIOutput): 显示适配器的输出,一般对应一个显示器
  • 交换链 (IDXGISwapChain): 用来暂存要显示到输出窗口/全屏幕的 1 到多个 Surface 的对象
    • g_pSwapChain->GetBuffer 可以拿到表示 Back Buffer 的 ID3D11Texture
    • g_pd3dDevice->CreateRenderTargetView 来创建一个封装该 Texture 的 ID3D11RenderTargetView
    • g_pd3dDeviceContext->OMSetRenderTargets 来设置管线的 RenderTarget
    • g_pd3dDeviceContext->RSSetViewports 来设置管线的 Viewport

GameApp::Init()

  • D3DApp::Init()
    • InitMainWindow()
      • RegisterClass(WNDCLASS *): 注册
      • AdjustWindowRect()
      • CreateWindow()
      • ShowWindow()
      • UpdateWindow()
    • InitDirect3D()
      • D3D11CreateDevice(): 采用 11.1 的 Feature Level,不行则降级

        该函数会返回 Immediate Context (ID3D11DeviceContext), 设备 (ID3D11Device) 和特性等级

      • ID3D11Device::CheckMultisampleQualityLevels: 查询给定 DXGI_FORMAT 是否支持给定倍数的 MSAA

      • 将前面的 ID3D11Device Cast 到 IDXGIDevice

        An IDXGIDevice interface implements a derived class for DXGI objects that produce image data.

      • IDXGIDevice::GetAdapter 拿到 IDXGIAdapter

        The IDXGIAdapter interface represents a display subsystem (including one or more GPUs, DACs and video memory).

      • IDXGIAdapter::GetParent 拿到 IDXGIFactory1

        这里的 GetParentIDXGIAdapter 作为 IDXGIObject 的方法,可以获得构造它的工厂类。

        The IDXGIFactory1 interface implements methods for generating DXGI objects.

      • 尝试将 IDXGIFactory1 Cast 到 IDXGIFactory2 (DXGI 1.2 新增)

        • 如果支持 DXGI 1.2,则用 CreateSwapChainForHwnd 来创建交换链
        • 否则,用 CreateSwapChain 来创建交换链

          这两个函数都可以创建窗口 / 全屏幕交换链;DXGI 1.2 增加了新的、到其它输出目标的交换链创建功能,所以这里进行了重构。

          也要注意,不同 DirectX 可以支持的交换链的交换行为类型是不同的。大体上,交换链的交换行为可以分为

          • DISCARD vs SEQUENTIAL: 可以参考 StackExchange,区别就是一个驱动可以放心扔掉,另一个必须保留回读可能
          • FILP vs BLIT (Bit Block Transfer): 决定是用交换指针还是数据拷贝的方法来从交换链被 Present 的 Surface 中拿取数据
      • IDXGIFactory1::MakeWindowAssociation 来取消让 DXGI 接收 Alt-Enter 的键盘消息并且切换窗口和全屏模式

      • D3D11SetDebugObjectName()

        • ID3D11DeviceChild::SetPrivateData(WKPDID_D3DDebugObjectName, ...) 来设置资源的内部数据,这里是调试名称
      • DXGISetDebugObjectName()

        • IDXGIObject::SetPrivateData(WKPDID_D3DDebugObjectName, ...) 来设置资源的内部数据,这里是调试名称
      • OnResize()

        • IDXGISwapChain::ResizeBuffers()
        • IDXGISwapChain::GetBuffer() 拿到 ID3D11Texture2D 形式的 Back Buffer
        • IDXGISwapChain::CreateRenderTargetView() 创建绑定到上面 Back Buffer 的 Texture 的渲染目标视图
        • D3D11SetDebugObjectName 来设置 Back Buffer 的调试名称
        • ID3D11Device::CreateTexture2D() 来创建深度模板缓冲 (Depth Stencil Buffer),类型 ID3D11Texture2D,包含大小,MipLevel,采样描述等
        • ID3D11Device::CreateDepthStencilView() 来创建前面缓冲对应的深度模板视图
        • ID3D11DeviceContext::OMSetRenderTargets() 来将渲染目标视图和深度木板视图绑定到管线
        • ID3D11DeviceConetxt::RSSetViewports() 绑定 Viewport 信息到光栅器状态
        • D3D11SetDebugObjectName() 设置调试前面各种视图对象的对象名
  • InitEffect()
    • CreateShaderFromFile(): 传入 CSO (Compiled Shader Object) 和 Shader 文件,输出 ID3DBlob *
      • 如果有缓存,则用 D3DReadFileToBlob 装入,并返回
      • D3DCompileFromFile(): 编译并生成 ID3DBlob 对象
      • 如果指定了缓存路径,则 D3DWriteBlobToFile() 进行输出

        分别创建了 vs_5_0ps_5_0 Shader Model 的 Shader Blob

    • ID3D11Device::CreateVertexShader(),根据 Shader Bytecode 创建 ID3D11VertexShader 对象

      注意这个函数支持传入 Class Linkage,这是一种在 Shader 间共享类型和变量的机制,在 Shader Model 5 被引入。更详细的用法可以参考 Dynamic Linking Class | MSDN

      TODO: 研究一下

    • ID3D11Device::CreateInputLayout() 传入输入元素描述符和 Shader,传出 ID3D11InputLayout 对象
    • ID3D11Device::CreatePixelShader() ,根据 Shader Bytecode 创建 ID3D11PixelShader 对象
  • InitResource()
    • ID3D11Device::CreateBuffer() 创建顶点缓冲区 (ID3D11Buffer) ,并传入初始化数据
    • ID3D11Device::CreateBuffer() 创建索引缓冲区 (ID3D11Buffer) ,并传入初始化数据
    • ID3D11DeviceContext::IASetIndexBuffer() 设置 Immediate Context 绑定索引缓冲区
    • ID3D11Device::CreateBuffer() 创建常量缓冲区 (ID3D11Buffer) ,不是用初始化数据
      • 此处设置 D3D11_BUFFER_DESCCPUAccessFlagsD3D11_CPU_ACCESS_WRITE,让 CPU 可以改变其值
    • ID3D11DeviceContext::IASetVertexBuffers() 设置顶点缓冲区,stride 和 offset
    • ID3D11DeviceContext::IASetPrimitiveTopology() 设置图元类型
    • ID3D11DeviceContext::IASetInputLayout() 设置输入布局
    • ID3D11DeviceContext::VSSetShader() 绑定顶点着色器到管线
    • ID3D11DeviceContext::VSSetConstantBuffers() 设置常量缓冲区

      这里当然是拿着 ID3D11Buffer 去设置

    • ID3D11DeviceContext::PSSetShader() 设置像素着色器
    • D3D11SetDebugObjectName() 将 Input Layout, Shader 和 Buffer 设置好调试用名字

GameApp::Run()

关于 Windows 消息机制的相关介绍可以参考 About messages and message queues | MSDN

运行首先依赖 Windows 窗口程序本身的主事件循环(PeekMessage() => TranslateMessage() => DispatchMessage())。

主窗口的消息处理函数中,主要会处理:

  • WM_SIZE: 如果在 WM_ENTERSIZEMOVEWM_EXITSIZEMOVE 中间,则忽略,否则调用 OnResize() 重新配置交换链并绑定到管线
  • WM_ACTIVATE: 窗口不活跃时暂停渲染
  • WM_DESTROY: 窗口退出消息

如果没有待处理的窗口消息,则会进入:

  • CalculateFrameStats(): 根据定时器计算时长并更新窗口标题
  • UpdateScene(): 更新场景 (主要是更新常量缓冲)
    • 计算更新后的常量缓冲区值
    • ID3D11DeviceContext::Map 传入 Constant Buffer 对象

      MSDN: Gets a pointer to the data contained in a subresource, and denies the GPU access to that subresource.

      这里要指定映射类型 (CPU 可读,CPU 可写,CPU 可写且原内容可放弃);不过,这里还有一种类型,叫做 D3D11_MAP_WRITE_NO_OVERWRITE,这块 MSDN 的文档有比较详细的解释。

      也可以看看这篇知乎专栏作为参考。

    • memcpy(mappedData.pData, &cpuCBuffer, sizeof(cpuConstBuffer)) 将数据拷贝到 D3D11_MAPPED_SUBRESOURCE::pData 成员处
    • ID3D11DeviceContext::Unmap 解除内存映射
  • DrawScene(): 绘制场景
    • ID3D11DeviceContext::ClearRenderTargetView(): 用给定颜色清空渲染目标视图
    • ID3D11DeviceContext::ClearDepthStencilView(): 用给定深度和模板值清空深度模板视图
    • ID3D11DeviceContext::DrawIndexed(): 绘制给定的立方体
    • IDXGISwapChain::Present(SyncInterval=0, flags=0): 告知交换链已经完成绘制,可以呈现,并且要求立即呈现

      IDXGISwapChain::Present: Presents a rendered image to the user.

论文阅读 | ICARUS: NeRF 硬件加速器

简介

本篇文章介绍了 NeRF 硬件加速的实现。

NeRF 回顾

Neural Radiance Field,简称 NeRF,最开始在 ECCV 2020 上被提出,提出了以神经网络编码辐射场的一种技术,并且将其运用到了基于图片的场景重建等多个领域中,是近年来受关注度相当高的一篇工作。

NeRF 的网络部分输入为 5D: 位置 $ (x,y,z) $ 和朝向 $ (\theta, \phi) $,输出为该位置的 RGB 颜色和密度。

NeRF 在给定相机位置下最终渲染的输出用类似体渲染 (Volumetric Rendering) 的办法来实现。

NeRF 体渲染

对给定的相机光线 $ {\bf r}(t) = {\bf o} + t{\bf d} $ 来说,最终输出的颜色 $ {\bf C}(r) $ 以下式表示:

$$ C({\bf r}) = \int_{t_n}^{t_f} T(t) \sigma({\bf r}(t)) {\bf c}({\bf r}(t), {\bf d}) dt $$

其中:

  • $ T(t) = \exp (-\int_{t_n}^{t} \sigma({\bf r}(s)) ds ) $ 为光线从 $ t_n $ 能打到 $ t $ 的概率
    • 比如说,如果射线穿过的部分密度都比较大,那 $ T(t) $ 就会比较小
  • $ \sigma({\bf r}(t)) $ 是该 $ t $ 对应的点 $ {\bf r}(t) $ 的密度
  • $ {\bf c}({\bf r}(t), {\bf d}) $ 是网络给定方向和位置后输出的 RGB 颜色值
  • $ t_n $ 和 $ t_f $ 分别为射线进入和射出 NeRF 有效区域的包围盒时所对应的最近和最远参数值

不过这个积分显然不能很容易的解析求解,NeRF 的做法是采用数值积分的那一套。

首先,利用分层抽样 (stratified sampling) 的技术,将 $ [t_n, t_f] $ 分成 $ N $ 个均匀的小区间,然后在每个小区间均匀采样出一个 $ t_i $ 出来。

然后,用下面的量 $ \hat C({\bf r}) $ 来估计上面的 $ C({\bf r}) $:

$$ \hat C({\bf r}) = \sum_{i=1}^{N} T_i (1-\exp(-\sigma_i \delta_i)) {\bf c}_i $$

其中:

  • $ T_i = \exp(- \sum_{j=1}^{i-1} \sigma_j \delta_j) $
  • $ \delta_i = t_{i+1} - t_i $ 为两临近采样点的距离

为什么会变成这个形式?可以参考 arXiv 上的 Volume Rendering Digest (for NeRF)

原文中提到,从所有的 $ ({\bf c}_i, \delta_i) $ 对考虑的话,$ \hat C(r) $ 的计算显然是可微的,并且可以看成从最开始一直用 $ \alpha_i = 1 - \exp(\sigma_i \delta_i) $ 的透明度往上面做 alpha blending。

NeRF 网络

网络部分用位置编码 (Positional Encoding) + Coarse MLP + Fine MLP。

位置编码

位置编码用来改善网络对高频细节的学习效果。

位置编码层可以如下描述:

$$ \gamma(p) = (\sin(2^0 \pi p), \cos(2^0 \pi p), ..., \sin(2^{L-1} \pi p), \cos(2^{L-1} \pi p)) $$

Coarse & Fine MLP

NeRF 同时使用两个 MLP 来表示场景,一个粗粒度 MLP 和一个细粒度 MLP。

渲染的时候,首先用分层抽样的办法,在粗粒度网络中用前面提到的体渲染方法进行渲染,并且得到输出 $ \hat C_c(r) $:

$$ \hat C_c(r) = \sum_{i=1}^{N_c} w_i c_i, \quad w_i = T_i (1-\exp(\sigma_i \delta_i)) $$

然后,计算归一化权重 $ \hat w_i = w_i / \sum_{i=1}^{N_c} w_i $,并且用计算好的归一化权重作为概率分布函数 (cumulative distribution function),再在这条直线上采样 $ N_f $ 个位置,将这 $ N_c + N_f $ 个位置送入细粒度 MLP 进行推理,再用前面的办法渲染得到最终的颜色值。

损失函数

采用简单的把 Coarse MLP 和 Fine MLP 与真实值之间的 $ L^2 $ 损失直接加起来的办法。

ICARUS

NeRF 计算过程回顾

  1. 对像素所发出射线上的采样,得到点 $ ({\bf p}_1, …, {\bf p}_N) $
  2. 查询 MLP 网络:$ ({\bf p}_i, {\bf d}_i) \to ({\bf c_i}, \sigma_i) $
  3. 进行多次 alpha-blending

架构设计

架构设计时主要有以下目标:

  1. “端到端” - 芯片输入位置和方向,输出像素颜色,减少片上片外数据交换的额外开销(计算时间、功耗)
  2. 使用定点数 - 有效降低浮点数运算开销
  3. 架构设计要一定灵活性,尽量兼容比较多的 NeRF 衍生网络

如何使用定点数?

目前的实现是将在 GPU 上训练好的 NeRF 的权重进行量化 (quantization),再导出。不过,目前也有一些工作在 quantization-aware training 方面,可能对这个网络的训练过程有所帮助。

位置编码单元 (PEU)

设计位置编码单元 (Positional Encoding Unit, PEU) 的目的是在 PEU 前和 PEU 后的向量维数增加了很多倍(对原 NeRF 来说位置是 20 倍,方向是 8 倍),如果在 ICARUS 内部进行计算的话,可以减少很大一部分外部存储传输,降低传输总用时。

PEU 部件主要在做这件事:

$$ \phi(x; A) = [\cos A^T x, \sin A^T x] $$

其中 $ A $ 一般为一个行数比列数多的矩阵,用来升维。

PEU 单元对应的设计如下 (Fig. 4(b)):

可以看到,就是矩阵乘法单元和 CORDIC 单元的组合。

一些关于矩阵乘和 CORDIC 单元的大概印象:
矩阵乘:有很多工作,比如搜索 Systolic array 等等

具体设计上来说,ICARUS 支持对 dim=3 和 dim=6 的两种输入进行位置编码,并且扩展到 dim=128。PEU 内部设计有两个 3x128 的内存块和 6 组 MAC (Multiply-ACcumulate) 单元,当计算 dim=6 的输入时会全部启用,当计算 dim=3 的输入时只启用一半。

MLP Engine

MLP 引擎主要进行 $ f(Wx+b) $ 类型的计算。

MLP 引擎包含有:

  • 一个 Multi-output Network block (MONB),负责计算中间的隐藏层
  • 一个 Single-output network block (SONB),负责计算最后的输出层
    • 不继续用 MONB 的原因是,全连接的 MONB 比只输出一个数字的 SONB 面积要大得多
  • 两个 activation memory block

对于 MLP 计算来说,实现是这样的:

首先,将 MLP 的权重拆成 64 x 64 的小块,方便硬件上的复用,并且同样的权重可以被多组输入向量复用,从而降低内存带宽开销,代价方面只需要暂存该 batch 内的中间结果就可以(这里选择 batch_size=128)。

每个 64 x 64 的矩阵-向量乘法再进行分片,变成按矩阵列分割的 64 个列向量 - 向量的内积乘法(即 $ [\alpha_1 … \alpha_{64}] [x_1 … x_{64}]^T $,每个 $ \alpha_i x_i $ 的部分和用一个 RMCM 模块实现:

大概来说,是因为乘法可以变成移位加法:

$$ 3x = 1x << 1 + 1x $$

所以权重 load 进来的作用就是预先选择好路径上的移位和加法器,然后数据从这些器件中流过去就行。

另一个优化是高一半的移位和加法路径直接用上一次的值来替换,然后网络训练的时候也作此改动。这样可以节省 1/3 的面积,同时输出基本没有视觉质量损失。

SONB 的架构基本上和 MONB 差不多,只是 RMCM 块用不到了,用普通的向量乘法块就可以了。

Volume Rendering Unit

VRU 模块主要要负责下面的计算:

这里,他处理成下面的形式:

然后用上面的网络计算。

原型验证

验证平台使用的是 Synopsys HAPS-80 S104,验证时使用的工艺是 40nm CMOS 工艺。

论文阅读 | 基于物理的特征线渲染

简介

本篇文章介绍了基于物理的特征线渲染方法。

特征线渲染是一种非真实感渲染技术,常常被用在剪影、产品效果图等一些需要特殊的艺术效果的场合。

本篇文章提出的,基于路径方法的特征线渲染方法,是基于如下的两方面观察:

  1. 从路径的角度出发,现有的特征线渲染方法将特征线处理成了光源
  2. 特征线相交测试可以对任意的边开展,而不仅仅是在屏幕空间中

基于上面的观察,本文提出的方法

  1. 对一整个路径中每条路径段分别进行和特征线的相交测试
  2. 将交到的特征线视为吸收所有入射光,然后辐射用户自定义颜色的光源

TL;DR: 用 Path Tracing 做描边,把要描的边处理成光源,让描边也有景深、色散和反射等效果。

算法描述

算法的基本架构很简单。

从传统的 Path Tracer 出发:

  1. 对每个 path segment 依次进行和特征线的相交判断,并且
  2. 如果相交,则将特征线视为理想光源,并不再追下面的光源

相交判断

从该 path segment 出发,以固定扩张率和当前路径总长度做一锥体,寻找锥体中的特征线。

实现上,本文采用从锥体较窄的一端发射查询射线,并且判断交点是否为特征线上的点的方法来进行判断,可能和 cone tracing 比较相似。

采样权重修改

前面提到,“如果相交,则将特征线视为理想光源,并不再追下面的光源”。本文中会将这种情况整条采样路径每个点的 pdf 值处理成和打到刚好有光源位于这里的情况完全一致。

不过,这样会让整个估计变成有偏估计,因为还存在有特征线(i.e. 有光源)但是没有采样到的情况,这种情况使用正常 pdf 会让最后相机处接收的 Irradiance 期望偏小,也就是特征线会比无偏的情况更不明显。

比较幸运的是,通过加密相交判断中发射的查询射线的数量,可以渐进的趋于无偏的情况。

特征线判据

特征线的判断通过锥体中采样到的点和本 path segment 的起点和终点联合进行判断,主要有 MeshID, Albedo, Normal 和 Depth 四个方面的判据。

其中 $ t_{\text{depth}} $ 文中提到有一个较为启发的设置方法。

各项的效果如图所示:

效果

可以参考本文 Teaser:

可以看到,本文渲染的特征线有色散、景深模糊、反射等基于物理的效果。

未来的工作

文章最后主要提及了如下的 Future Work:

  • 其它路径采样方法 (i.e. BDPT)
  • 特征线锥形区域估计改进
  • 特征线区域缓存
  • 特征线模型改进
    • 反射 / 半透特征线模型等
  • 将 lens blur 和色散效果集成到 Stylized Focus
    • Stylized Focus 主要通过多个光栅化 pass 的叠加来实现风格化的景深和聚焦效果
让 Hexo 支持内联 LaTeX 数学公式的 Markdown

经过一周多陆陆续续的折腾,到现在本博客基本实现了博客 Markdown 渲染和 Typora, VSCode 等的默认预览中数学公式的表现一致。

Note: 大于号(>)小于号(<)目前没有做额外的转义(它们本身是 HTML 标签的结束和开始),但是这个用 \lt\gt 就行了,所以有点懒得改。

如果要改的话,就对 math 的处理加上这两个东西的 escape 就好,MathJaX 本身会识别出 &lt;&gt; 的。

简单来说,我分别魔改了 markedhexo-renderer-marked 两个包,实现了内联 LaTeX 的正确 Tokenize 和 Renderer (i.e. 什么也不做,原样输出),再由浏览器里运行的 MathJaX 3 来进行 LaTeX 渲染。

魔改后的版本支持块公式 (block math) $$ ... $$ 和内联公式 (inline math) $ ... $,并且内联公式内部的 _ 不会和 Markdown 对 _ 的使用冲突。

下面是渲染 Maxwell 方程组的示例:

$$ \begin{aligned} \nabla \cdot \mathbf{E} &= \frac {\rho} {\varepsilon_0} \\ \nabla \cdot \mathbf{B} &= 0 \\ \nabla \times \mathbf{E} &= -\frac{\partial \mathbf{B}} {\partial t} \\ \nabla \times \mathbf{B} &= \mu_0\left(\mathbf{J} + \varepsilon_0 \frac{\partial \mathbf{E}} {\partial t} \right) \end{aligned} $$

上面的 $ \mathbf{E} $ 是电场强度,$ \mathbf{B} $ 是磁感应强度,$ \mu_0$ 是真空磁导率,$ \epsilon_0 $ 是真空介电系数。

markedhexo-renderer-marked 使用的 Markdown 渲染器,它负责把 Markdown 渲染成 HTML。

为什么要新造一个轮子?

Google 一圈,现在有的 Hexo 内联 LaTeX 的方案都不是很让人满意:

  1. hexo-renderer-kramed 使用的 kramed 从 2016 年开始已经没有再更新了
  2. marked 表示不会加入对 $ ... $$$ ... $$ 的支持 (markedjs/marked, Issue #722)
  3. hexo-renderer-pandoc 需要用户自己安装 pandocpandoc 本身很庞大,并且是 Haskell 编写,本文作者表示改不动;另外,直接 out-of-box 的装上之后,块公式 pandoc 总是会多生成 \[ ... \] 的 pair,决定弃坑
  4. 网络上还存在一些 patch 方案,比如这里,直接把 marked 的 inline rule 改掉,让其不再将 _ 作为合法的强调标志(比如 _asdf_ 之类就不会渲染成 asdf 了)
  5. 其实也可以 摆大烂,把所有 LaTeX 和 Markdown 冲突的关键字都用反斜杠转义掉

可以看到都不是太优雅。

改动简介

其实一开始想给 hexo-renderer-marked 写插件,但是它只支持 extend TokenizerRenderer,把那些乱七八糟规则再写一遍又很难维护,所以最后放弃了这个想法。

主要是对 marked 进行改动,让其支持 $$ .. $$$ .. $ 的 Tokenize,并且能无转义的输出。

marked 采用正则表达式不断匹配的方式进行词法分析,对于部分块对象会继续进行行内的词法分析。词法分析后的 Toekn 流会送到 Renderer 进行输出。

详细可以看 libreliu/marked 上面的提交。

由于人比较懒,没有在 npm 上加自己的包,而是直接

1
npm install github:libreliu/hexo-renderer-marked-math#master --save

这个的缺点是每次 npm update hexo-renderer-marked-math 都要重新拉,并且版本管理上不是很友好。不过只是自己用的话其实无所谓。

(作为菜鸡)踩过的坑

  1. NodeJS 的 require 在找不到 index.js 时,会去 package.json 中查找 main 字段,并且加载对应的模块。

    可以注意到,markedmain./lib/marked.cjs,这个文件需要运行 npm run build 生成。

  2. 调试 Promise 链可以采用 Bluebird 的Promise.longStackTraces()

相关工作

这里在 2015 年对 VSCode 的 Markdown 渲染的 patch 对本更改有参考意义。

math-marked这个项目在我基本写完之后才看到,不过作者没有基于原来的 commit 继续改,后续升版本会比较费劲。

论文阅读 | Slang

简介

Slang 是在 HLSL 之上扩展的着色器语言,其旨在保证没有额外性能损失的情况下,解决现代游戏引擎和实时渲染应用中出现的,不同光源 / 材质组合出现的 Shader 代码膨胀、晦涩难懂的基于宏的复用等问题。

Slang 针对原 HLSL,主要增加了以下的新特性:

  1. 带可选成员类型约束的接口系统 (Interfaces)
    • 以及 extension 关键字来覆盖接口系统的默认行为,提供最大灵活性
  2. 泛型系统 (Generics)
  3. 显式参数块 (Explicit Parameter Blocks)
  4. Slang 编译器和提供运行时类型特化支持的编译器运行时 API
  5. (论文发表后新增) 语法糖和其它易用性改进
    • 类 C# 的 getter/setter 语法糖
    • 运算符重载
    • 模块系统
  6. (论文发表后新增) CUDA, OptiX 等非传统 Shader 编译目标

从上面可以看出,Slang 自论文发布后还存在有功能演进,说明作为 Shader 语言本身还是有一定生命力的。

截至 2022 年 7 月 31 日,Slang 的 GitHub 仓库 共有 978 个 Star,235 Open Issue 和 284 Closed Issue,最后一次提交在两天前,证明项目还是比较活跃的。

Note: 据我浅薄的了解,HLSL 也在不断迭代新的功能,如 HLSL 2021template 泛型支持等。不过 GLSL 好像没啥大动作(?)

Slang 语言特性介绍

接口系统 (Interfaces)

Slang 的接口表示一种约定,比如约定里面会有某种特定函数原型的函数实现,某种特定的成员结构体等,与 “traits” 的语言概念比较接近。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// define a interface
interface IFoo
{
int myMethod(float arg);
}

// declare that MyType have conformance to interface IFoo
struct MyType : IFoo
{
int myMethod(float arg)
{
return (int)arg + 1;
}
}

泛型系统 (Generics)

泛型系统可以实现同时支持不同类型实例的函数。特别的,可以约束传入的形参类型为实现某种接口的类型,如下面的 myGenericMethod 约束 T 为实现 IFoo 接口的类型。

1
2
3
4
5
6
7
8
9
10
// define
int myGenericMethod<T: IFoo>(T arg)
{
return arg.myMethod(1.0);
}

// invoke
MyType obj;
int a = myGenericMethod<MyType>(obj); // OK, explicit type argument
int b = myGenericMethod(obj); // OK, automatic type deduction

同时,Slang 还支持类似 C++ 的非类型模板形参的泛型参数输入,比如下面的 N

1
2
3
4
struct Array<T, let N : int>
{
T arrayContent[N];
}

函数,struct 等语言组件都支持泛型。

Note: HLSL 本身就支持作为 struct 的 arg 拥有成员函数。

显式参数块 (Explicit Parameter Blocks)

这个特性在 Slang 官方的语言文档中没有着重强调,主要是在 Slang 的这篇论文中强调了。

现代图形 API(D3D12, Vulkan)对于 Shader 的输入参数是以 “parameter block” 的形式来组织的(例:Vulkan 中术语为 Descriptor Set),一个 Shader 的输入可以由多个 Descriptor Set 组成。

考虑到场景的绘制过程中,有一部分 Shader 参数是不变的(比如同一个绘制到主 RenderTarget 的 Pass 中摄像机的位置),那么把这些不变的参数单独拿出来组织成一个 Parameter Block,把剩下的一些变化频率不太一致的另一些参数(比如模型的 modelMatrix)拿出来作为一个或多个 Parameter Block 的话,就可以降低一部分绘制时的开销。

但是手工组织 Shader 的 layout(特别是对于不同的材质,我们有不同的 Shader 要用)是比较繁琐的,Slang 则对这个 Parameter Binding 和 Parameter Block 有专门的设计(Shader Parameters),可以方便的自动推导符合程序员设计要求的 layout 和 block。

编译器和运行时 API

Slang 提供了功能丰富的运行时 API,其功能经过一些演进和论文中描述的也不是特别一致了。

Slang 的运行时 API 大概提供了如下机制:

  1. 运行时 Shader 编译和特化 API
    • 比如,运行时要对新的材质类型重新编译 Shader,就可以使用这些 API
  2. 反射机制
    • 可以获得某段 Slang Shader 中的函数、Shader 入口参数等信息

语法糖

Slang 的文档中描述了不少 Slang 的语法糖和易用性改进。

类似 C# 的 getter / setter 语法糖

1
2
3
4
5
6
7
8
9
10
struct MyType
{
uint flag;

property uint highBits
{
get { return flag >> 16; }
set { flag = (flag & 0xFF) + (newValue << 16); }
}
};

基于全局函数的运算符重载

HLSL 2021 支持了基于成员函数的运算符重载:[Announcing HLSL 2021

而 GLSL 截至 2022 年 7 月 31 日还在咕咕:Operator overloading · Issue #107 · KhronosGroup/GLSL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct MyType
{
int val;
__init(int x) { val = x; }
}

MyType operator+(MyType a, MyType b)
{
return MyType(a.val + b.val);
}

int test()
{
MyType rs = MyType(1) + MyType(2);
return rs.val; // returns 3.
}

模块机制

Slang 支持一个简单的模块机制,可以把要 import 的目标模块(就是一个 .slang 的 Slang Shader)中的定义导入当前单元。如果该模块此时再被其它模块 import 的话,这些被导入的模块是不会导入到它的“上一层”的单元中的。

1
2
3
4
5
6
7
// MyShader.slang

// 正常的导入
import YourLibrary;

// 当然,也可以导入时覆盖前面描述的行为
__export import SomeOtherModule;

重构 Falcor 渲染器

Falcor 是一个基于 D3D12 的实时渲染器。

根据 README,Vulkan 实验性支持在进行中。

作者从 Falcor 的 2.0.2 版本出发,重构了 5400 行着色器代码。

改进主要集中在如下方面:

  1. 将一个大的 Parameter Block 拆分成 per-material 的 block

  2. Falcor 原来的 Material 采用层次化的设计,每一层 (e.g. GGX, Lambertian, Phong) 都和下一层进行 blend,而渲染时,根据不同的 material 要 dispatch 不同的 shader 时,采用了很多 #define 和基于文本的 Shader Varient Cache 的查询环节。

    作者重构时将这套系统用 Slang 的泛型系统重构,并且将标准材质的 Varient 用非类型形参编码成了若干个 int,进而加快了查询速度。

  3. 针对与 Material 类似的技术,实现了光源上的特化,在场景中只有某种类型光源的时候采用静态特化好的 Shader 变体,减少运行时判断的开销

性能

通过重构 Falcor 渲染器,在 NVIDIA 的 ORCA 场景上的测试表明

  • 每帧 CPU 执行时间降低了 30%
  • 光源和材料特化改进对部分场景的 GPU 时间有加速作用

可扩展性

作者分别在重构前和重构后的 Falcor 上实现了(基于 LTC 方法的)多边形面光源,

  • 重构前:需要改动 7 处,4 个文件,246 行
  • 重构前,但加入光源分离机制:需要改动 8 处,5 个文件,253 行
  • 重构后:只需要改动 1 处,1 个文件,249 行

结论

We believe that all real-time graphics programmers could benefit from
a new generation of shader compilation tools informed by these
ideas. —— Slang: language mechanisms for extensible real-time shading systems

论文阅读 | Dynamic Diffuse Global Illumination with Ray-Traced Irradiance Fields

简介

本篇文章提供了一种高效的计算动态物体和动态光源情形下的全局光照的方法。

框架

DDGI light probe

DDGI 是一种利用 light probe(光照探针)进行动态全局光计算的方法。

对于位于 $\mathrm{x’}$ 位置的 light probe,所有的出射方向可以视为从 probe 所在位置到以 probe 所在位置为中心的单位球面上点构成的向量的集合。此时,构造一个 $S^2 \to R^2$ 的映射,使得球面的八个扇区分别映射到八面体的八个面上,这个映射被称为八面体映射 (octahedron mapping)。

文章中描述到,八面体映射的好处,在于可以将球面以比较均匀的参数化映射到正方形上去,方便之后将每个方向相对应的量储存到 2D 纹理上面去。

通过八面体映射,就可以将 probe 每个方向的信息存储在正方形的纹理贴图上了。

不过,在这篇文章中,出于性能考虑,作者采用了类似 Variance Shadow Mapping 的方法,极大压缩了纹理贴图的分辨率,同时对于每个 probe 的贴图的每个方向,分别存放

  1. $E_i(\mathrm{x'}, w)$: probe 以 $\omega$ 方向为天顶的半球的入射 irradiance
  2. $r(\omega)$: probe 在 $\omega$ 方向对应的最近邻图元的距离在半球面的均值
    • 也就是 $\int d(x’, \omega) d \omega$,其中 $d(x, \omega): R^3 \times \Omega \to R$ 为在 $x$ 处沿 $\omega$ 方向到最近邻图元的距离
  3. $r^2(\omega)$: probe 在 $\omega$ 方向对应的最近邻图元的距离的平方在半球面的均值

三组信息。

Recall: radiance 和 irradiance

  • Radiance (辐射率): 单位面积单位立体角辐射功率,$ d\Phi / (dS d\Omega) $
  • Irradiance (辐照度): 单位面积辐射功率 $ d\Phi / dS $

利用 probe 进行间接光计算

前面提到 probe 中存储的信息为 probe 所在位置中各个方向的入射 irradiance。如果把场景中各处的 irradiance 看成一个 irradiance 场,那么现在要处理的问题就是给定场在某些位置的值,插值出其他位置的值的过程。

对于漫反射,只需要关心入射 irradiance 而不需要具体的 radiance,所以只需要待着色图元的全局光入射 irradiance 信息。

irradiance 场大概可以这样描述:$R^3 \times S^2 \to Spectrum$

输入是 (位置, 方向),输出是 Spectrum (e.g. RGBSpectrum)

可以想象到,如果场本身的变化相对于 probe 间距离来说变化比较缓慢,那么方法就会工作的比较好。

不过,也有一些会导致变化较快的情况:

  1. 图元本身与 probe 所成夹角
  2. 图元被某些物体遮挡

所以,DDGI 提出了这样的框架来进行着色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// float3 n = shading normal, X = shading point, P = probe location
float4 irradiance = float4(0);
for (each of 8 probes around X) {
float3 dir = P – X;
float r = length(dir);
dir *= 1.0 / r;

// smooth backface
float weight = (dot(dir, n) + 1) * 0.5;

// adjacency
weight *= trilinear(P, X);

// visibility (Chebyshev)
float2 temp = texelFetch(depthTex, probeCoord).rg;
float mean = temp.r, mean2 = temp.g;
if (r > mean) {
float variance = abs(square(mean) – mean2);
weight *= variance / (variance + square(r – mean));
}
irradiance += sqrt(texelFetch(colorTex, probeCoord) * weight;
}
return square(irradiance.rgb * (1.0 / irradiance.a));

irradiance.a 的作用是什么..?

很多权重我理解是为了视觉效果,应该和物理正确没什么太大关系…

这里的也不是最终的版本(还要加上 normal bias),slides 里面提供了更加魔改的版本,不知道 RTXGI 里面是不是有更进一步的魔改

Chebyshev 项分析

Chebyshev 不等式 (one-tailed version):

$$ P(x > t) \le \frac{\sigma^2}{\sigma^2 + (t-\mu)^2} $$

可以参考 GAMES202 中关于 Variance Soft Shadow Mapping 的部分

相当于小于平均值时认为没有遮挡,大于平均值时按 Chebyshev 不等式的上界来估算被遮挡概率。

有没有更好的估计方法?为什么这样估计是最好的?

各个项效果对比

原论文中有各项的作用展示:

其中 classic irradiance probe 应该就是只有三线性插值的结果。

动态更新 probe 信息

每一帧,DDGI 会进行如下的操作:

  1. 从 $m$ 个活跃 probe 中,每个 probe 发射 $n$ 条光线,然后存储 $n \times m$ 个交点处的表面元信息(位置,法线)到一个类似 G-buffer 的结构中
    • 发射光线时采用每帧不同的 pattern,最大限度避免锯齿
      • 作者采用 “stochastically-rotated Fibonacci spiral pattern”

        不过作者 2017 年的文章中并没有详细说明此处的具体实现,需要阅读作者的代码

  2. 对表面元信息进行直接光和间接光计算
    • 直接光:
      • 点光源和方向光光源:利用该 G-buffer 进行普通的 deferred rendering + variance shadow mapping
      • 面积光光源:使用下面间接光方法,第一跳时考虑面积光
    • 间接光:采用周围的 probe 信息进行计算
      • 和前面一节描述的方法一致
    • (多跳)间接光:通过 3 中每次用 Moving Average 方法来更新,实现多跳的信息传播
  3. 更新这 $m$ 个活跃 probe 对应的纹理贴图
    • 利用 alpha-blending, $\alpha$ 取 0.85 到 0.98
    • newIrradiance[texelDir] = lerp(oldIrradiance[texelDir], Sum(ProbeRays(max(0,texelDir · rayDir) ∗ rayRadiance)...), alpha)

符号说明: lerp(a, b, alpha) = a * alpha + b * (1-alpha)

对比

  • Real-Time Global Illumination using Precomputed Light Field Probes
    • McGuire 这篇 2017 年的工作中关于 GI 的部分和这篇文章很像,只是当时他在 light probe 中存储比较高分辨率的最近邻图元到 probe 距离,并且用这个距离来进行基于 probe 阵列的 ray trace,而不是采用硬件 ray trace。
    • 并且他在这篇工作中提到,可以采用将 BSDF 分解成 diffuse + glossy (所有不 diffuse 的项),对 glossy 用其它方法来处理 (比如 raytrace + post filter) 来实现整个场景的 GI。
测试嵌套 ul 和 li

这是一些正常的测试文本。

  • first class ul
    • second class ul
      • third class ul
  1. 测试 first class li
    测试缩进后文字显示
    • 测试 inner class ul
      • 测试更内部显示
  2. 测试 first class li
    测试缩进后文字显示
    • 测试 inner class ul
      • 测试更内部显示
论文阅读 | EARS

简介

本篇文章主要介绍了一种在离线渲染中优化 Path Tracing 中 Russian Roulette 和 Splitting 的方法。

首先,Splitting 即在 Path Tracing 的过程中,到某个 bounce 后,分叉出多条光线进行 trace,最后计算该点光照贡献时按权重进行平均的一种技术。

作为例子,考虑如下的场景(图源论文):

该场景中,池底的表面为漫反射材质,但是路径中其它的部分的 BSDF / BRDF 都比较趋向于 Delta 分布。这时,如果可以在绿色点进行 Splitting,对不需要 Splitting 的路径实现复用,就可以帮助以更小的开销实现较低方差的渲染。

在每次 bounce 时,PathTracer 都需要进行一个决策:

  • (Russian Roulette) 是否需要截止这条光线?以多少概率截止?
  • (Splitting) 是否需要将这条光线分裂成多份?如果需要的话,分裂成多少份?

这些因子显然是和场景相关的,而选择好这些因子可以加速 Path Tracing 的收敛过程。

Formulation

TODO

论文阅读 | Generalized RIS

简介

本篇文章主要是扩展了 ReSTIR 和 ReSTIR GI 中用到的 Resampled Importance Sampling 在图形学中的理论基础。