写在前面

好久没有更新shadertoy系列了,我万万没想到有童鞋还惦记着它。。。之前说过希望可以一周更新一篇,现在看来是不怎么可能了,一个月更新一篇的希望比较大(不要再相信我了。。。)

我把之前实现的这个系列上传到了GitHub(https://github.com/candycat1992/Shadertoy_Lab)上,有兴趣的可以去下载下来。当然,也希望有网友可以一起贡献这个项目。

GitHub上这个项目大部分灵感来源于shadertoy(https://www.shadertoy.com),也有一些是配合博客里的一些文章讲解的,也有一些是在原shadertoy里面的例子扩展而来的。总之,每个lab我都会在README里面给出相关的参考资料。

项目链接:https://github.com/candycat1992/Shadertoy_Lab


好啦,我们来看一下这一篇里面要讲的例子。如同题目所讲,我们的目标是模拟水彩风格的效果。当然,这里实现的只是简化版本后的实现,我们只实现了渲染部分。

参考资料:

[1] https://www.shadertoy.com/view/XdSSWd 
[2] Curtis C J, Anderson S E, Seims J E, et al. Computer-generated watercolor[C]// Proceedings of the 24th annual conference on Computer graphics and interactive techniques. ACM Press/Addison-Wesley Publishing Co., 1997.

论文研讨:Computer-Generated Watercolor

这个例子来源于一篇著名的论文,也就是1997年的Computer-Generated Watercolor。年代虽然很久远了,但是这篇论文开启了用计算机模拟水彩画的先河,后面陆陆续续又有很多论文被发表出来,但几乎都可以看到这篇论文的影子。

这篇论文主要可以分为四个部分:

  1. 首先,描述了水彩颜料的物理性质,并从艺术角度给出了一些水彩画的风格特性;
  2. 给出了如何模拟这些特性的方法;
  3. 具体描述了对水彩和颜料(pigment)的物理模拟算法;
  4. 描述了如何渲染这些颜料。

而本文其实只是实现了最后一个部分,在本节后面的内容里,我会简略介绍下论文里其他三个方面的内容。如果读者对这方面研究有兴趣的话,还是强烈建议去阅读原论文。

水彩的物理属性

水彩画(watercolor paint ,也被简称为watercolor)是一种比较常见的艺术风格。一幅水彩画涉及到了两种材质:

  • 水彩纸(watercolor paper)。它并不是由木材制作而成的,而是通过把亚麻布或者棉花捣碎成细小的纤维的来的。这种材质非常容易吸收液体,为了防止颜料迅速蔓延,因此还给这些纸张进行上浆(sizing)。

  • 颜料(pigment)。这是一种固体材质,由很多很小的单独的粒子组成。这些水彩颜料通常由0.05到0.5微米的粉末构成,它们可以渗透水彩纸,但一旦附着在纸上,扩散速度就会下降。

除此之外,水彩画有一些特点,例如:

  • 干笔画(Dry brush)。如果使用较干的画笔画在粗糙的纸上,那么会出现一些不规则的空隙和粗糙的边界效果。

  • 边界颜色较深(Edge darkening)。如果使用较湿的画笔画在较干的纸面上,在纸的浆料和水的表面张力的作用下,颜料不会继续扩散,并在边缘处留下一圈颜色更深的沉淀痕迹。

模拟

在论文中,作者提出使用三个图层来模拟水彩画中颜料的流动:

  • 第一层是shallow-water layer。在这一层中,水和颜料会在纸张表面扩散流动。

  • 第二层是pigment-deposition layer。在这一层中,颜料会沉淀进入和释放出纸张。

  • 第三层是capillary layer。在这一层中,被纸张吸收的水会通过毛细管作用被继续扩散。(这一层仅仅用于模拟水彩画的回流效果。)

在模拟时,作者使用了很多参数来控制模拟效果,例如颜料的扩散速度、画笔压力、纸张的高度、颜料密度、液体饱和度、液体容量等等。

关于纸张的模拟,作者使用了一种简单的模型,即高度场的方法,并使用了Perlin噪声(Ken Perlin. An image synthesizer. In SIGGRAPH ’85 Proceedings, pages 287–296. July 1985.)和Worley的多孔纹理(Steven P. Worley. A cellular texturing basis function. In SIGGRAPH ’96 Proceedings, pages 291–294. 1996.)来生成。这种方法非常常见。

算法

有了上述这些参数之后,就可以进行算法模拟的部分。主循环部分在每个时间步内,会进行四个计算步骤:

  1. 在shallow-water layer移动液体(Move Water)。

  2. 在shallow-water layer移动颜料(Move Pigment)。

  3. 在pigment-deposition layer传递颜料(Transfer Pigment)。这一步会模拟颜料的吸收和释放。

  4. 在capillary layer模拟毛细流动(Simulate Capillary Flow)。这一步会模拟回流现象等。

具体的算法还是要参考论文,本文不涉及这些算法的实现。

渲染

以上的内容只是为了完整性,而与这篇博客相关的只有渲染部分。

当经过上面的算法后,我们可以得到每个区域的颜料厚度。

作者使用了Kubelka-Munk(KM)模型来渲染颜料。在论文中,作者为每个颜料指定了两个系数:吸收系数(absorption coefficients)K和散射系数(scattering coefficients)S。K和S都是三维属性,分别表示颜料吸收和散射的能量。

指定颜料的光学属性

虽然K和S系数通常是经验决定的,但作者允许让用户来指定:通过选择希望的“unit thickness”(单位厚度)的颜料在黑白背景下的外观来决定。具体方法是,给定用户选择的两个RGB颜色 Rw (在白色背景下的颜色)和 Rb (在黑色背景下的颜色),K和S系数可以靠下面的等式来得到:

S=1b⋅coth−1(b2−(a−Rw)(a−1)b(1−Rw))K=S(a−1)其中,a=12(Rw+Rb−Rw+1Rb)b=a2−1−−−−−√

作者在论文里给出了一些计算出来的不同样色、不同属性颜料的KS系数。如下图所示(图片来源《Computer-Generated Watercolor》):

这些颜料是不同类型的,例如: 
* 不透明颜料(Opaque paints)。类似Indian Red(上图中的b),在白色和黑色区域都有类似的颜色。这种颜料都具有高散射、高吸收的属性。

  • 透明颜料(Transparent paints)。类似Quinacridone Rose(上图中的a),在白色背景下有颜色,在黑色背景下几乎是黑色的。这种颜料的scattering波长都很低,而absorption分量很高,并和它们的颜色是补集。

  • 干涉颜料(Interference paints)。类似Interference Lilac(上图中的l),在白色背景下几乎是白色的,而在黑色背景下是有颜色的。

光学的颜料层混合

一旦给定了一个一定厚度x的颜料层以及它的散射和吸收系数S和K,我们就可以按下面的公式计算该颜料层的反射比R和透射比T。

R=sinhbSxcT=bc,其中c=asinhbSx+bcoshbSx

对于两个相邻的层,我们可以按下面公式来计算合成后的颜料层的R和T:

R=R1+T21R21−R1R2T=T1T21−R1R2

Shader的实现

下面的内容会解释如何使用Unity Shader来实现上面的渲染部分。

从上面的渲染算法中可以看出,实际上渲染部分只涉及到了每个区域的颜料厚度x以及颜料的系数K和S。在下面的实现中,我们使用了论文中提供的一系列K和S。为此,我们在shader中定义了如下变量:

<code class="language-C++ hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Table of pigments </span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// from Computer-Generated Watercolor. Cassidy et al.</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// K is absorption. S is scattering</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// a</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_QuinacridoneRose vec3(0.22, 1.47, 0.57)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_QuinacridoneRose vec3(0.05, 0.003, 0.03)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// b</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_IndianRed vec3(0.46, 1.07, 1.50)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_IndianRed vec3(1.28, 0.38, 0.21)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// c</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_CadmiumYellow vec3(0.10, 0.36, 3.45)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_CadmiumYellow vec3(0.97, 0.65, 0.007)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// d</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_HookersGreen vec3(1.62, 0.61, 1.64)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_HookersGreen vec3(0.01, 0.012, 0.003)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// e</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_CeruleanBlue vec3(1.52, 0.32, 0.25)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_CeruleanBlue vec3(0.06, 0.26, 0.40)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// f</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_BurntUmber vec3(0.74, 1.54, 2.10)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_BurntUmber vec3(0.09, 0.09, 0.004)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// g</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_CadmiumRed vec3(0.14, 1.08, 1.68)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_CadmiumRed vec3(0.77, 0.015, 0.018)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// h</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_BrilliantOrange vec3(0.13, 0.81, 3.45)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_BrilliantOrange vec3(0.009, 0.007, 0.01)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// i</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_HansaYellow vec3(0.06, 0.21, 1.78)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_HansaYellow vec3(0.50, 0.88, 0.009)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// j</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_PhthaloGreen vec3(1.55, 0.47, 0.63)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_PhthaloGreen vec3(0.01, 0.05, 0.035)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// k</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_FrenchUltramarine vec3(0.86, 0.86, 0.06)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_FrenchUltramarine vec3(0.005, 0.005, 0.09)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// l</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_InterferenceLilac vec3(0.08, 0.11, 0.07)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_InterferenceLilac vec3(1.25, 0.42, 1.43)</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li></ul>

对于颜料厚度,则是基于distance field的方法,再通过一些计算来模拟Edge darkening的效果。

我们首先来看颜料层的反射比R和透射比T。代码如下:

<code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Kubelka-Munk reflectance and transmitance model</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> KM(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> K, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> S, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> x, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">out</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> R, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">out</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> T) {<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> a = (K + S) / S;<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> b = <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">sqrt</span>(a * a - <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>));<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> bSx = b * S * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span>(x);<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> sinh_bSx = my_sinh(bSx);<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> c = a * sinh_bSx + b * my_cosh(bSx);R = sinh_bSx / c;T = b / c;}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li></ul>

它的输入有三个:该区域颜料的吸收系数K和散射系数S,颜料厚度x。输出该区域的反射比R和透射比T。有了上一节的公式,上面的代码就很简单了,就是带公式而已。

另一个公式是用于混合两个颜料层。相关代码如下:

<code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Kubelka-Munk model for optical compositing of layers</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> CompositeLayers(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> R0, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> T0, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> R1, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> T1, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">out</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> R, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">out</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> T) {<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> tmp = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>) / (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>) - R0 * R1);R = R0 + T0 * T0 * R1 * tmp;T = T0 * T1 * tmp;}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li></ul>

它的输入是两个颜料层的反射比和透射比,输出合成层的反射比和透射比。同样,上面的代码也是带公式而已。

至此,我们想要渲染出来画面还需要提供一个参数,就是KM函数中的颜料厚度x。在论文中,这个颜料厚度是通过一系列算法计算得到的。但在我们的实现中,我们简化了这一步,而使用基于distance field的方法来计算厚度。要模拟一定效果的水彩风格,我们需要模拟它的一些特性,例如Dry-brush和Edge darkening。我们这里选择一种取巧的方法,利用了噪声(模拟粗糙的边界效果)和一些数学计算(来模拟Edge darkening效果),而非原文中复杂的算法。

在我们的实现中,我们是在fragment shader中渲染图形的,也就是,我们处理的单位是逐像素的。当渲染一个图形时,我们需要几个步骤:

  1. 给定渲染区域的位置pos。为了模拟水彩画粗糙的边缘效果,我们会使用噪声函数来对屏幕坐标进行一些的噪声处理。例如:

    <code class="language-C++ hljs r has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">vec2 uv = fragCoord.xy / iResolution.xy
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">...</span>
    pos = uv * vec2(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + vec2(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * noise2d(uv * vec2(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

    uv是对屏幕坐标处理后得到的xy范围都在(0, 1)的坐标。为了计算当前位置的坐标,我们首先针对屏幕长宽进行处理,使得得到的坐标在x方向上的范围是(0, 1),y方向上的范围是(0, height/width)。然后,对结果添加了噪声处理。

    噪声函数noise2d的代码如下:

    <code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Simple 2d noise fbm (Fractional Brownian Motion) with 3 octaves</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> Noise2d(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span> p) {<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> t = <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">texture2D</span>(iChannel0, p).x;t += <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span> * <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">texture2D</span>(iChannel0, p * <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2.0</span>).x;t += <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.25</span> * <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">texture2D</span>(iChannel0, p * <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4.0</span>).x;<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> t / <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.75</span>;
    }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li></ul>

    这是一种非常简单的噪声实现。主要通过对一张噪声纹理采样,并使用了三层的octaves。这些内容可以在Perlin噪声的相关内容中找到,例如这里(http://freespace.virgin.net/hugo.elias/models/m_perlin.htm)和这里(http://stackoverflow.com/questions/16999520/in-need-of-fractional-brownian-noise-fbm-vs-perlin-noise-clarification)。我们简单解释一下,不同的octave表示不同的频率和振幅噪声,通过组合不同频率和振幅的噪声,我们就可以得到一个Perlin噪声。

  2. 在得到了区域坐标pos后,我们需要把它代入distance field的计算,得到距离值dist。例如:

    <code class="language-C++ hljs fix has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-attribute" style="box-sizing: border-box;">dist </span>=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;"> DistanceCircle(pos, vec2(0.2, 0.55), 0.08);</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

    DistanceCircle函数代码如下:

    <code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> DistanceCircle(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span> pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span> center, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> radius) {<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span> - <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">distance</span>(pos, center) / radius;
    }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

    它会计算pos相对于圆心在center、半径为radius的圆的距离,返回值 > 0时表示在圆内,返回值 < 0时表示在圆外。

    类似的距离计算函数还有DistanceLine(对应画直线)、DistanceSegment(对应画线段)和DistanceMountain(对应画一座由正弦函数得到的山脉)等。

  3. 在得到了距离值后,我们由此来判断一个点是否需要绘制水彩。但为了后面的渲染,我们还需要把这个距离值转换成颜料厚度,这是通过BrushEffect函数得到的。例如:

    <code class="language-C++ hljs mel has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">circle</span> = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

    BrushEffect函数代码如下:

    <code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Simulate edge darkening effect</span>
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Input: dist < 0 outer area, dist > 0 inner area</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> BrushEffect(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> dist, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> x_avg, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> x_var) {<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Only when abs(dist) < 1.0/10.0, x > 0.0</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Means that the edges have more thickness of pigments</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> x = <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">max</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.0</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span> - <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10.0</span> * <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">abs</span>(dist)); x *= x;x *= x;<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> (x_avg + x_var * x) * <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">smoothstep</span>(-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.01</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.002</span>, dist);
    }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul>

    BrushEffect不仅会把距离值dist变换到颜料厚度,也会负责模拟Edge darkening效果。它的输入是上一步计算而得的dist(dist < 0表示在渲染图形的外部,dist > 0表示在内部),以及平均颜料厚度x_avg和边缘厚度变化x_var。计算过程是:

    1. 第一行首先根据dist计算初始的边缘颜料厚度x,它的范围是(0, 1)。当dist的绝对值小于1/10时(即靠近边界处),x大于0;否则x等于0。我们也可以调整公式中的参数10,值越小,Edge darkening的范围就越广。

    2. 后面两行进一步处理边缘颜料厚度x,这是通过自乘两次得到的。这样可以进一步快读收紧Edge darkening的范围。

    3. 计算返回值即颜料的厚度。首先我们通过smoothstep函数来控制厚度的整体变化,具体是,当dist小于-0.01时,返回0,当大于0.002返回1,否则返回0到1之间的值。-0.01和0.002的选择并不是完全任意的,我们一般选择一正一负来处理边界,同时正数(这里是0.002)的数值通常要小于负数的绝对值(这里是|-0.01|),这是为了让来模拟出颜料在边界处的扩散速度非线性下降的效果。然后,我们把该值和(x_avg + x_var * x)的结果相乘。其中,x_avg是渲染图形内部绝大多数区域的颜料厚度,而x_var用于控制边界处的颜料厚度(因为边界处的颜料厚度要大于内部),x_var越大,边界出的Edge darkening效果越明显。在我们的实现中,一般取x_avg为0.2,取x_var为0.1。当然,如果我们想要模拟出粗糙感,也可以传噪声进去,例如:

    <code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> mountains = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

    至此,我们就得到了该区域的颜料厚度。同样,为了简单的模拟颜料不均匀分布的特点,我们也可以进一步对结果值进行噪声处理。例如:

    <code class="language-C++ hljs fix has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-attribute" style="box-sizing: border-box;">mountains *</span>=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;"> 0.65 + 0.35 * Noise2d(uv * vec2(0.2));</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

    注意其中的系数0.65和0.35,它们的和需要是1。如果把0.35的调大、把0.65调小,粗糙感就越强烈。

  4. 最后,我们只需要把颜料厚度,和选择的KS系数传递给KM函数得到该颜料层的反射比和透射比。如果需要和之前的颜料层混合,再代入CompositeLayers函数混合即可。例如:

    <code class="language-C++ hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">KM(K_HansaYellow, S_HansaYellow, circle, <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">R1</span>, T1)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
    CompositeLayers(<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">R0</span>, T0, <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">R1</span>, T1, <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">R0</span>, T0)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>

实现效果

shadertoy中原作者的绘制结果我在Unity中重现后是下面这样的(调整了一些参数):

上述场景的绘制代码如下:

<code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/// </span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/// First Scene</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">///</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Background</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> background = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>));
KM(K_CeruleanBlue, S_CeruleanBlue, background, R0, T0);pos = uv + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.04</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));
dist = DistanceMountain(pos, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span>);
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> mountains = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));
mountains *= <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.45</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.55</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>));
KM(K_HookersGreen, S_HookersGreen, mountains, R1, T1);
CompositeLayers(R0, T0, R1, T1, R0, T0);pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));
dist = DistanceCircle(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.55</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.08</span>);
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> circle = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>);
KM(K_HansaYellow, S_HansaYellow, circle, R1, T1);
CompositeLayers(R0, T0, R1, T1, R0, T0);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li></ul>

我在原shader的基础上做了一些扩展,例如给出了原论文中所有样例的KS系数,给出了更过的距离计算函数。通过这些的组合,可以得到更多的效果。例如:

上述场景的绘制代码如下:

<code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">        <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/// </span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/// Second Scene</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">///</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Background</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> background = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>));KM(K_HansaYellow, S_HansaYellow, background, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Edge roughness: 0.04</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.04</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));dist = DistanceCircle(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.15</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Average thickness: 0.2, edge varing thickness: 0.2</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> circle = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Granulation: 0.85</span>circle *= <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.15</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.85</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>));KM(K_CadmiumRed, S_CadmiumRed, circle, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Edge roughness: 0.03</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.03</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));dist = DistanceCircle(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.4</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.15</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Average thickness: 0.3, edge varing thickness: 0.1</span>circle = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Granulation: 0.65</span>circle *= <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.35</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.65</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>));KM(K_HookersGreen, S_HookersGreen, circle, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Edge roughness: 0.02</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));dist = DistanceCircle(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.6</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.15</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Average thickness: 0.3, edge varing thickness: 0.2</span>circle = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Granulation: 0.45</span>circle *= <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.55</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.45</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>));KM(K_FrenchUltramarine, S_FrenchUltramarine, circle, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Opaque paints, e.g. Indian Red</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span>)));dist = DistanceSegment(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>), <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.4</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.25</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.03</span>);<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> line = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>);KM(K_IndianRed, S_IndianRed, line, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Transparent paints, e.g. Quinacridone Rose</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>)));dist = DistanceSegment(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span>), <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.4</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.55</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.03</span>);line = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>);KM(K_QuinacridoneRose, S_QuinacridoneRose, line, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Interference paints, e.g. Interference Lilac</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));dist = DistanceSegment(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.6</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.55</span>), <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.8</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.4</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.03</span>);line = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>);KM(K_InterferenceLilac, S_InterferenceLilac, line, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li><li style="box-sizing: border-box; padding: 0px 5px;">43</li><li style="box-sizing: border-box; padding: 0px 5px;">44</li><li style="box-sizing: border-box; padding: 0px 5px;">45</li><li style="box-sizing: border-box; padding: 0px 5px;">46</li><li style="box-sizing: border-box; padding: 0px 5px;">47</li><li style="box-sizing: border-box; padding: 0px 5px;">48</li><li style="box-sizing: border-box; padding: 0px 5px;">49</li><li style="box-sizing: border-box; padding: 0px 5px;">50</li><li style="box-sizing: border-box; padding: 0px 5px;">51</li><li style="box-sizing: border-box; padding: 0px 5px;">52</li><li style="box-sizing: border-box; padding: 0px 5px;">53</li><li style="box-sizing: border-box; padding: 0px 5px;">54</li><li style="box-sizing: border-box; padding: 0px 5px;">55</li><li style="box-sizing: border-box; padding: 0px 5px;">56</li><li style="box-sizing: border-box; padding: 0px 5px;">57</li><li style="box-sizing: border-box; padding: 0px 5px;">58</li></ul>

注意到上面对参数的调整和不同图形效果的区别,例如边界颜色更深、边界粗糙感和整体颗粒感等等。

完整的代码读者可以在https://github.com/candycat1992/Shadertoy_Lab中的WaterColorScene找到。

写在最后

本文实现了水彩风格的渲染部分。对于颜料厚度的计算则是通过简单的数学计算来模拟的,当然,这样得到的效果也并不真实。想要得到更加真实的效果,需要配合更复杂的算法,具体可以参见上面的论文及其它发表的论文。

这篇文章只是抛砖引玉,从它的实现我们可以学到KM模型在实时渲染中的实现,以及噪声的简单应用。在本文中,我们的实现都是基于distance field的方法,也就是说我们画出的图形其实都是用数学表达式计算而得的。读者可以添加更过的函数来绘制更复杂的图形。如果想要实现那种用户交互的应用,也可以使用其它方法来计算颜料厚度。

最后,希望这篇文章可以对大家有所帮助~

写在前面

好久没有更新shadertoy系列了,我万万没想到有童鞋还惦记着它。。。之前说过希望可以一周更新一篇,现在看来是不怎么可能了,一个月更新一篇的希望比较大(不要再相信我了。。。)

我把之前实现的这个系列上传到了GitHub(https://github.com/candycat1992/Shadertoy_Lab)上,有兴趣的可以去下载下来。当然,也希望有网友可以一起贡献这个项目。

GitHub上这个项目大部分灵感来源于shadertoy(https://www.shadertoy.com),也有一些是配合博客里的一些文章讲解的,也有一些是在原shadertoy里面的例子扩展而来的。总之,每个lab我都会在README里面给出相关的参考资料。

项目链接:https://github.com/candycat1992/Shadertoy_Lab


好啦,我们来看一下这一篇里面要讲的例子。如同题目所讲,我们的目标是模拟水彩风格的效果。当然,这里实现的只是简化版本后的实现,我们只实现了渲染部分。

参考资料:

[1] https://www.shadertoy.com/view/XdSSWd 
[2] Curtis C J, Anderson S E, Seims J E, et al. Computer-generated watercolor[C]// Proceedings of the 24th annual conference on Computer graphics and interactive techniques. ACM Press/Addison-Wesley Publishing Co., 1997.

论文研讨:Computer-Generated Watercolor

这个例子来源于一篇著名的论文,也就是1997年的Computer-Generated Watercolor。年代虽然很久远了,但是这篇论文开启了用计算机模拟水彩画的先河,后面陆陆续续又有很多论文被发表出来,但几乎都可以看到这篇论文的影子。

这篇论文主要可以分为四个部分:

  1. 首先,描述了水彩颜料的物理性质,并从艺术角度给出了一些水彩画的风格特性;
  2. 给出了如何模拟这些特性的方法;
  3. 具体描述了对水彩和颜料(pigment)的物理模拟算法;
  4. 描述了如何渲染这些颜料。

而本文其实只是实现了最后一个部分,在本节后面的内容里,我会简略介绍下论文里其他三个方面的内容。如果读者对这方面研究有兴趣的话,还是强烈建议去阅读原论文。

水彩的物理属性

水彩画(watercolor paint ,也被简称为watercolor)是一种比较常见的艺术风格。一幅水彩画涉及到了两种材质:

  • 水彩纸(watercolor paper)。它并不是由木材制作而成的,而是通过把亚麻布或者棉花捣碎成细小的纤维的来的。这种材质非常容易吸收液体,为了防止颜料迅速蔓延,因此还给这些纸张进行上浆(sizing)。

  • 颜料(pigment)。这是一种固体材质,由很多很小的单独的粒子组成。这些水彩颜料通常由0.05到0.5微米的粉末构成,它们可以渗透水彩纸,但一旦附着在纸上,扩散速度就会下降。

除此之外,水彩画有一些特点,例如:

  • 干笔画(Dry brush)。如果使用较干的画笔画在粗糙的纸上,那么会出现一些不规则的空隙和粗糙的边界效果。

  • 边界颜色较深(Edge darkening)。如果使用较湿的画笔画在较干的纸面上,在纸的浆料和水的表面张力的作用下,颜料不会继续扩散,并在边缘处留下一圈颜色更深的沉淀痕迹。

模拟

在论文中,作者提出使用三个图层来模拟水彩画中颜料的流动:

  • 第一层是shallow-water layer。在这一层中,水和颜料会在纸张表面扩散流动。

  • 第二层是pigment-deposition layer。在这一层中,颜料会沉淀进入和释放出纸张。

  • 第三层是capillary layer。在这一层中,被纸张吸收的水会通过毛细管作用被继续扩散。(这一层仅仅用于模拟水彩画的回流效果。)

在模拟时,作者使用了很多参数来控制模拟效果,例如颜料的扩散速度、画笔压力、纸张的高度、颜料密度、液体饱和度、液体容量等等。

关于纸张的模拟,作者使用了一种简单的模型,即高度场的方法,并使用了Perlin噪声(Ken Perlin. An image synthesizer. In SIGGRAPH ’85 Proceedings, pages 287–296. July 1985.)和Worley的多孔纹理(Steven P. Worley. A cellular texturing basis function. In SIGGRAPH ’96 Proceedings, pages 291–294. 1996.)来生成。这种方法非常常见。

算法

有了上述这些参数之后,就可以进行算法模拟的部分。主循环部分在每个时间步内,会进行四个计算步骤:

  1. 在shallow-water layer移动液体(Move Water)。

  2. 在shallow-water layer移动颜料(Move Pigment)。

  3. 在pigment-deposition layer传递颜料(Transfer Pigment)。这一步会模拟颜料的吸收和释放。

  4. 在capillary layer模拟毛细流动(Simulate Capillary Flow)。这一步会模拟回流现象等。

具体的算法还是要参考论文,本文不涉及这些算法的实现。

渲染

以上的内容只是为了完整性,而与这篇博客相关的只有渲染部分。

当经过上面的算法后,我们可以得到每个区域的颜料厚度。

作者使用了Kubelka-Munk(KM)模型来渲染颜料。在论文中,作者为每个颜料指定了两个系数:吸收系数(absorption coefficients)K和散射系数(scattering coefficients)S。K和S都是三维属性,分别表示颜料吸收和散射的能量。

指定颜料的光学属性

虽然K和S系数通常是经验决定的,但作者允许让用户来指定:通过选择希望的“unit thickness”(单位厚度)的颜料在黑白背景下的外观来决定。具体方法是,给定用户选择的两个RGB颜色 Rw (在白色背景下的颜色)和 Rb (在黑色背景下的颜色),K和S系数可以靠下面的等式来得到:

S=1b⋅coth−1(b2−(a−Rw)(a−1)b(1−Rw))K=S(a−1)其中,a=12(Rw+Rb−Rw+1Rb)b=a2−1−−−−−√

作者在论文里给出了一些计算出来的不同样色、不同属性颜料的KS系数。如下图所示(图片来源《Computer-Generated Watercolor》):

这些颜料是不同类型的,例如: 
* 不透明颜料(Opaque paints)。类似Indian Red(上图中的b),在白色和黑色区域都有类似的颜色。这种颜料都具有高散射、高吸收的属性。

  • 透明颜料(Transparent paints)。类似Quinacridone Rose(上图中的a),在白色背景下有颜色,在黑色背景下几乎是黑色的。这种颜料的scattering波长都很低,而absorption分量很高,并和它们的颜色是补集。

  • 干涉颜料(Interference paints)。类似Interference Lilac(上图中的l),在白色背景下几乎是白色的,而在黑色背景下是有颜色的。

光学的颜料层混合

一旦给定了一个一定厚度x的颜料层以及它的散射和吸收系数S和K,我们就可以按下面的公式计算该颜料层的反射比R和透射比T

R=sinhbSxcT=bc,其中c=asinhbSx+bcoshbSx

对于两个相邻的层,我们可以按下面公式来计算合成后的颜料层的R和T:

R=R1+T21R21−R1R2T=T1T21−R1R2

Shader的实现

下面的内容会解释如何使用Unity Shader来实现上面的渲染部分。

从上面的渲染算法中可以看出,实际上渲染部分只涉及到了每个区域的颜料厚度x以及颜料的系数K和S。在下面的实现中,我们使用了论文中提供的一系列K和S。为此,我们在shader中定义了如下变量:

<code class="language-C++ hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Table of pigments </span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// from Computer-Generated Watercolor. Cassidy et al.</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// K is absorption. S is scattering</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// a</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_QuinacridoneRose vec3(0.22, 1.47, 0.57)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_QuinacridoneRose vec3(0.05, 0.003, 0.03)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// b</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_IndianRed vec3(0.46, 1.07, 1.50)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_IndianRed vec3(1.28, 0.38, 0.21)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// c</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_CadmiumYellow vec3(0.10, 0.36, 3.45)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_CadmiumYellow vec3(0.97, 0.65, 0.007)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// d</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_HookersGreen vec3(1.62, 0.61, 1.64)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_HookersGreen vec3(0.01, 0.012, 0.003)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// e</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_CeruleanBlue vec3(1.52, 0.32, 0.25)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_CeruleanBlue vec3(0.06, 0.26, 0.40)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// f</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_BurntUmber vec3(0.74, 1.54, 2.10)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_BurntUmber vec3(0.09, 0.09, 0.004)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// g</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_CadmiumRed vec3(0.14, 1.08, 1.68)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_CadmiumRed vec3(0.77, 0.015, 0.018)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// h</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_BrilliantOrange vec3(0.13, 0.81, 3.45)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_BrilliantOrange vec3(0.009, 0.007, 0.01)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// i</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_HansaYellow vec3(0.06, 0.21, 1.78)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_HansaYellow vec3(0.50, 0.88, 0.009)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// j</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_PhthaloGreen vec3(1.55, 0.47, 0.63)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_PhthaloGreen vec3(0.01, 0.05, 0.035)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// k</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_FrenchUltramarine vec3(0.86, 0.86, 0.06)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_FrenchUltramarine vec3(0.005, 0.005, 0.09)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// l</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> K_InterferenceLilac vec3(0.08, 0.11, 0.07)</span><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> S_InterferenceLilac vec3(1.25, 0.42, 1.43)</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li></ul>

对于颜料厚度,则是基于distance field的方法,再通过一些计算来模拟Edge darkening的效果。

我们首先来看颜料层的反射比R和透射比T。代码如下:

<code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Kubelka-Munk reflectance and transmitance model</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> KM(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> K, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> S, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> x, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">out</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> R, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">out</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> T) {<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> a = (K + S) / S;<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> b = <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">sqrt</span>(a * a - <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>));<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> bSx = b * S * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span>(x);<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> sinh_bSx = my_sinh(bSx);<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> c = a * sinh_bSx + b * my_cosh(bSx);R = sinh_bSx / c;T = b / c;}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li></ul>

它的输入有三个:该区域颜料的吸收系数K和散射系数S,颜料厚度x。输出该区域的反射比R和透射比T。有了上一节的公式,上面的代码就很简单了,就是带公式而已。

另一个公式是用于混合两个颜料层。相关代码如下:

<code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Kubelka-Munk model for optical compositing of layers</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> CompositeLayers(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> R0, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> T0, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> R1, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> T1, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">out</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> R, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">out</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> T) {<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span> tmp = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>) / (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec3</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>) - R0 * R1);R = R0 + T0 * T0 * R1 * tmp;T = T0 * T1 * tmp;}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li></ul>

它的输入是两个颜料层的反射比和透射比,输出合成层的反射比和透射比。同样,上面的代码也是带公式而已。

至此,我们想要渲染出来画面还需要提供一个参数,就是KM函数中的颜料厚度x。在论文中,这个颜料厚度是通过一系列算法计算得到的。但在我们的实现中,我们简化了这一步,而使用基于distance field的方法来计算厚度。要模拟一定效果的水彩风格,我们需要模拟它的一些特性,例如Dry-brush和Edge darkening。我们这里选择一种取巧的方法,利用了噪声(模拟粗糙的边界效果)和一些数学计算(来模拟Edge darkening效果),而非原文中复杂的算法。

在我们的实现中,我们是在fragment shader中渲染图形的,也就是,我们处理的单位是逐像素的。当渲染一个图形时,我们需要几个步骤:

  1. 给定渲染区域的位置pos。为了模拟水彩画粗糙的边缘效果,我们会使用噪声函数来对屏幕坐标进行一些的噪声处理。例如:

    <code class="language-C++ hljs r has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">vec2 uv = fragCoord.xy / iResolution.xy
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">...</span>
    pos = uv * vec2(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + vec2(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * noise2d(uv * vec2(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

    uv是对屏幕坐标处理后得到的xy范围都在(0, 1)的坐标。为了计算当前位置的坐标,我们首先针对屏幕长宽进行处理,使得得到的坐标在x方向上的范围是(0, 1),y方向上的范围是(0, height/width)。然后,对结果添加了噪声处理。

    噪声函数noise2d的代码如下:

    <code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Simple 2d noise fbm (Fractional Brownian Motion) with 3 octaves</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> Noise2d(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span> p) {<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> t = <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">texture2D</span>(iChannel0, p).x;t += <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span> * <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">texture2D</span>(iChannel0, p * <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2.0</span>).x;t += <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.25</span> * <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">texture2D</span>(iChannel0, p * <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4.0</span>).x;<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> t / <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.75</span>;
    }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li></ul>

    这是一种非常简单的噪声实现。主要通过对一张噪声纹理采样,并使用了三层的octaves。这些内容可以在Perlin噪声的相关内容中找到,例如这里(http://freespace.virgin.net/hugo.elias/models/m_perlin.htm)和这里(http://stackoverflow.com/questions/16999520/in-need-of-fractional-brownian-noise-fbm-vs-perlin-noise-clarification)。我们简单解释一下,不同的octave表示不同的频率和振幅噪声,通过组合不同频率和振幅的噪声,我们就可以得到一个Perlin噪声。

  2. 在得到了区域坐标pos后,我们需要把它代入distance field的计算,得到距离值dist。例如:

    <code class="language-C++ hljs fix has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-attribute" style="box-sizing: border-box;">dist </span>=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;"> DistanceCircle(pos, vec2(0.2, 0.55), 0.08);</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

    DistanceCircle函数代码如下:

    <code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> DistanceCircle(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span> pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span> center, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> radius) {<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span> - <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">distance</span>(pos, center) / radius;
    }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

    它会计算pos相对于圆心在center、半径为radius的圆的距离,返回值 > 0时表示在圆内,返回值 < 0时表示在圆外。

    类似的距离计算函数还有DistanceLine(对应画直线)、DistanceSegment(对应画线段)和DistanceMountain(对应画一座由正弦函数得到的山脉)等。

  3. 在得到了距离值后,我们由此来判断一个点是否需要绘制水彩。但为了后面的渲染,我们还需要把这个距离值转换成颜料厚度,这是通过BrushEffect函数得到的。例如:

    <code class="language-C++ hljs mel has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">circle</span> = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

    BrushEffect函数代码如下:

    <code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Simulate edge darkening effect</span>
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Input: dist < 0 outer area, dist > 0 inner area</span>
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> BrushEffect(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> dist, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> x_avg, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> x_var) {<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Only when abs(dist) < 1.0/10.0, x > 0.0</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Means that the edges have more thickness of pigments</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> x = <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">max</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.0</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span> - <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10.0</span> * <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">abs</span>(dist)); x *= x;x *= x;<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> (x_avg + x_var * x) * <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">smoothstep</span>(-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.01</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.002</span>, dist);
    }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul>

    BrushEffect不仅会把距离值dist变换到颜料厚度,也会负责模拟Edge darkening效果。它的输入是上一步计算而得的dist(dist < 0表示在渲染图形的外部,dist > 0表示在内部),以及平均颜料厚度x_avg和边缘厚度变化x_var。计算过程是:

    1. 第一行首先根据dist计算初始的边缘颜料厚度x,它的范围是(0, 1)。当dist的绝对值小于1/10时(即靠近边界处),x大于0;否则x等于0。我们也可以调整公式中的参数10,值越小,Edge darkening的范围就越广。

    2. 后面两行进一步处理边缘颜料厚度x,这是通过自乘两次得到的。这样可以进一步快读收紧Edge darkening的范围。

    3. 计算返回值即颜料的厚度。首先我们通过smoothstep函数来控制厚度的整体变化,具体是,当dist小于-0.01时,返回0,当大于0.002返回1,否则返回0到1之间的值。-0.01和0.002的选择并不是完全任意的,我们一般选择一正一负来处理边界,同时正数(这里是0.002)的数值通常要小于负数的绝对值(这里是|-0.01|),这是为了让来模拟出颜料在边界处的扩散速度非线性下降的效果。然后,我们把该值和(x_avg + x_var * x)的结果相乘。其中,x_avg是渲染图形内部绝大多数区域的颜料厚度,而x_var用于控制边界处的颜料厚度(因为边界处的颜料厚度要大于内部),x_var越大,边界出的Edge darkening效果越明显。在我们的实现中,一般取x_avg为0.2,取x_var为0.1。当然,如果我们想要模拟出粗糙感,也可以传噪声进去,例如:

    <code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> mountains = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

    至此,我们就得到了该区域的颜料厚度。同样,为了简单的模拟颜料不均匀分布的特点,我们也可以进一步对结果值进行噪声处理。例如:

    <code class="language-C++ hljs fix has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-attribute" style="box-sizing: border-box;">mountains *</span>=<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;"> 0.65 + 0.35 * Noise2d(uv * vec2(0.2));</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

    注意其中的系数0.65和0.35,它们的和需要是1。如果把0.35的调大、把0.65调小,粗糙感就越强烈。

  4. 最后,我们只需要把颜料厚度,和选择的KS系数传递给KM函数得到该颜料层的反射比和透射比。如果需要和之前的颜料层混合,再代入CompositeLayers函数混合即可。例如:

    <code class="language-C++ hljs avrasm has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">KM(K_HansaYellow, S_HansaYellow, circle, <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">R1</span>, T1)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
    CompositeLayers(<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">R0</span>, T0, <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">R1</span>, T1, <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">R0</span>, T0)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>

实现效果

shadertoy中原作者的绘制结果我在Unity中重现后是下面这样的(调整了一些参数):

上述场景的绘制代码如下:

<code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/// </span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/// First Scene</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">///</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Background</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> background = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>));
KM(K_CeruleanBlue, S_CeruleanBlue, background, R0, T0);pos = uv + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.04</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));
dist = DistanceMountain(pos, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span>);
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> mountains = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));
mountains *= <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.45</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.55</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>));
KM(K_HookersGreen, S_HookersGreen, mountains, R1, T1);
CompositeLayers(R0, T0, R1, T1, R0, T0);pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));
dist = DistanceCircle(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.55</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.08</span>);
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> circle = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>);
KM(K_HansaYellow, S_HansaYellow, circle, R1, T1);
CompositeLayers(R0, T0, R1, T1, R0, T0);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li></ul>

我在原shader的基础上做了一些扩展,例如给出了原论文中所有样例的KS系数,给出了更过的距离计算函数。通过这些的组合,可以得到更多的效果。例如:

上述场景的绘制代码如下:

<code class="language-C++ hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">        <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/// </span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/// Second Scene</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">///</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Background</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> background = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>));KM(K_HansaYellow, S_HansaYellow, background, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Edge roughness: 0.04</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.04</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));dist = DistanceCircle(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.15</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Average thickness: 0.2, edge varing thickness: 0.2</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> circle = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Granulation: 0.85</span>circle *= <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.15</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.85</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>));KM(K_CadmiumRed, S_CadmiumRed, circle, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Edge roughness: 0.03</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.03</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));dist = DistanceCircle(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.4</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.15</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Average thickness: 0.3, edge varing thickness: 0.1</span>circle = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Granulation: 0.65</span>circle *= <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.35</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.65</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>));KM(K_HookersGreen, S_HookersGreen, circle, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Edge roughness: 0.02</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));dist = DistanceCircle(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.6</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.15</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Average thickness: 0.3, edge varing thickness: 0.2</span>circle = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Granulation: 0.45</span>circle *= <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.55</span> + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.45</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>));KM(K_FrenchUltramarine, S_FrenchUltramarine, circle, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Opaque paints, e.g. Indian Red</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.3</span>)));dist = DistanceSegment(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>), <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.4</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.25</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.03</span>);<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">float</span> line = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>);KM(K_IndianRed, S_IndianRed, line, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Transparent paints, e.g. Quinacridone Rose</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>)));dist = DistanceSegment(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span>), <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.4</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.55</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.03</span>);line = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>);KM(K_QuinacridoneRose, S_QuinacridoneRose, line, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Interference paints, e.g. Interference Lilac</span>pos = uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>, iResolution.y / iResolution.x) + <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.02</span> * Noise2d(uv * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>)));dist = DistanceSegment(pos, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.6</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.55</span>), <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">vec2</span>(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.8</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.4</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.03</span>);line = BrushEffect(dist, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.1</span>);KM(K_InterferenceLilac, S_InterferenceLilac, line, R1, T1);CompositeLayers(R0, T0, R1, T1, R0, T0);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li><li style="box-sizing: border-box; padding: 0px 5px;">43</li><li style="box-sizing: border-box; padding: 0px 5px;">44</li><li style="box-sizing: border-box; padding: 0px 5px;">45</li><li style="box-sizing: border-box; padding: 0px 5px;">46</li><li style="box-sizing: border-box; padding: 0px 5px;">47</li><li style="box-sizing: border-box; padding: 0px 5px;">48</li><li style="box-sizing: border-box; padding: 0px 5px;">49</li><li style="box-sizing: border-box; padding: 0px 5px;">50</li><li style="box-sizing: border-box; padding: 0px 5px;">51</li><li style="box-sizing: border-box; padding: 0px 5px;">52</li><li style="box-sizing: border-box; padding: 0px 5px;">53</li><li style="box-sizing: border-box; padding: 0px 5px;">54</li><li style="box-sizing: border-box; padding: 0px 5px;">55</li><li style="box-sizing: border-box; padding: 0px 5px;">56</li><li style="box-sizing: border-box; padding: 0px 5px;">57</li><li style="box-sizing: border-box; padding: 0px 5px;">58</li></ul>

注意到上面对参数的调整和不同图形效果的区别,例如边界颜色更深、边界粗糙感和整体颗粒感等等。

完整的代码读者可以在https://github.com/candycat1992/Shadertoy_Lab中的WaterColorScene找到。

写在最后

本文实现了水彩风格的渲染部分。对于颜料厚度的计算则是通过简单的数学计算来模拟的,当然,这样得到的效果也并不真实。想要得到更加真实的效果,需要配合更复杂的算法,具体可以参见上面的论文及其它发表的论文。

这篇文章只是抛砖引玉,从它的实现我们可以学到KM模型在实时渲染中的实现,以及噪声的简单应用。在本文中,我们的实现都是基于distance field的方法,也就是说我们画出的图形其实都是用数学表达式计算而得的。读者可以添加更过的函数来绘制更复杂的图形。如果想要实现那种用户交互的应用,也可以使用其它方法来计算颜料厚度。

最后,希望这篇文章可以对大家有所帮助~

【ShaderToy】水彩画相关推荐

  1. ShaderToy 水彩画

    好久没有更新shadertoy系列了,我万万没想到有童鞋还惦记着它...之前说过希望可以一周更新一篇,现在看来是不怎么可能了,一个月更新一篇的希望比较大(不要再相信我了...) 我把之前实现的这个系列 ...

  2. visual studio code安装shadertoy特效环境

    vscode安装shadertoy特效环境搭建教程 visual studio code安装shadertoy特效环境搭建教程 shadertoy介绍 搭建shadertoy环境 下载拓展插件 GLS ...

  3. 安检X光机下的春运归家图 宛如一幅幅水彩画

    1月22日,2019年春运第二天,在江苏扬州汽车站的安检处,所有进站旅客的行囊都要进行安检,接受X光检查,旅客的行囊通过X光安检机,不同物品不同材质在监视器上呈现出五颜六色的水彩画.图为在安检仪X光扫 ...

  4. ShaderToy效果学习(转成Unity URP) - MathEye

    来自IQ大神的小教程,效果图: shaderToy源码:https://www.shadertoy.com/view/lsfGRr 教程视频:https://www.bilibili.com/vide ...

  5. 【ShaderToy】跳动的心

    http://blog.csdn.net/candycat1992/article/details/44040273 写在前面 注:如果你还不了解ShaderToy,请看开篇. 作为ShaderToy ...

  6. 将ShaderToy中的Shader搬运到Unity

    一.ShaderToy作品 如果你对 Shader 有一定的了解,那么你或多或少听说过 shaderToy 这个网站,这个网站上有很多令人振奋的 shader 效果,而这些效果有可能只用了几行代码来实 ...

  7. 如何在Mac上创建水彩画?Art Text来告诉你!

    Art Text 4 for mac是适合所有人的平面设计软件.Art Text mac是专为字体,排版,文本模型和各种艺术文字效果而调整的图形设计软件.提供各种各样的随时可用的样式和材料,纹理,图标 ...

  8. 影评:雨天的水彩画 《爱有天意》观感(转)

    清澈的河水弯弯曲曲地向远方流淌,所过之处,两岸一片苍翠. 一座古老的小木桥,横跨在这河流之上,不知经历了多少年的风吹雨打. 河边的芦苇在微风中轻轻摇摆. 芦苇丛中,隐藏了一只小小的船,静静地泊在那里. ...

  9. ShaderToy Matlab OpenGL实现流动Love

    文章目录 0. 效果图 1. 数学理论 2. Matlab仿真 3. GLSL(OpenGL)仿真 总结 0. 效果图 该文章基于https://www.shadertoy.com/view/7l3G ...

最新文章

  1. Perl时间处理函数
  2. 3-3-完全二叉树结点数
  3. 基区宽度调制效应(厄尔利(Early)效应)
  4. 解决SecureCRT连接GNS3时SecureCRT标签窗口同名的问题
  5. 【Paper】2022_多无人机系统的分布式最优编队控制
  6. Shell脚本学习-阶段三
  7. 如何在SQL Server计算XX年第XX周是哪几天
  8. matlab二进制香农,香农编码及MATLAB实现.ppt
  9. 根据名称获取对应的拼音码首字母大写
  10. 如何修改路由器LAN口IP地址及原因?
  11. Java--静态代理和动态代理
  12. 爬取王者荣耀皮肤-点券领取
  13. Android识别字符串中的简体和繁体字
  14. DevExpress控件学习总结 z
  15. 6月19,观世音菩萨成道
  16. HTML自动点名代码,js+html实现点名系统功能
  17. 马克思主义哲学笔记(一)
  18. 梅西百货公司 - Macy‘s - Shop Fashion Clothing Accessories
  19. 将数学公式转换成c语言表达式,习题参考答案
  20. 对话 a16z 联创 Marc Andreessen:Web3 将产生基础性技术变革

热门文章

  1. 纯国产机器人“成团”进入北欧,深兰科技扬帆出海、稳步扩订单
  2. 机器学习 | 使用TensorFlow搭建神经网络实现鸢尾花分类
  3. Mac OS 安装 chrome 浏览器
  4. 仿qq空间java源码_android 发说说动态源码下载(高仿QQ空间,含图片多选)
  5. 战争磨盘五:擒贼擒王
  6. oracle定时任务记录
  7. [前缀和 乱搞]BZOJ4972 .小Q的方格纸
  8. 基于JAVA高校公共资源管理计算机毕业设计源码+数据库+lw文档+系统+部署
  9. MYSQL数据库周德伟教程后题_《MySQL数据库基础实例教程》编者:周德伟,覃国蓉著【摘要 书评 在线阅读】-苏宁易购图书...
  10. Java程序员的春天!我的世界java正版手机下载