半色调抖动与近景透明


Dithering4

话说,上个月神秘海域4 出了开发者日志, 其中的半色调近景透明技术让我非常感兴趣, 毕竟非常多游戏要用到这个功能, 比如很多RPG游戏,近景的墙壁如果遮挡住主角,就会半透明。 而且半透明又是很消耗性能的,这篇日志也提到使用半色调技术是因为性能考虑。

开发者日志地址 : http://www.paopaoche.net/tv/99983.html

之前一直没空,在研究了2天半色调技术后,终于渲染出了我的半色调皮卡丘!!
Dithering

之后,上周看 火猫tv 的 dota2 马尼拉比赛直播的时候,发现 dota2的新地图也用到了类似的技术, 如图中的柱子 �9�6
我想估计这个技术应该会慢慢铺开,不过玩家能不能马上接受这种像素风的半透明效果就另说咯。

Dithering2Dithering3

— 未完待续, 周末有空的话, 会补充 贝尔矩阵的求解过程,可以自己计算任意 偶次方长度的矩阵。

Unity3D 中的贴花效果

这周研究一下,以前从 Stage3D 时代就想做的效果 -> 贴花

具体是怎样的呢

decay3

就是可以贴合模型全新纹理,差不多是这样
于是我做了个 动态贴花插件,这样美术童鞋在做场景的时候就比较直观咯

decayCircle

具体应用的美术场景,可以看看这个图
decay1

decay2

贴花做法其实就是复制要贴花模型的某一部分三角形,然后绘制新的纹理
decay4

如何截取三角形呢

请不要无脑复制转载本文, 由 dreamfairy 原创, 转载请注明出处 本文地址 http://www.dreamfairy.cn/blog/2016/06/26/unity3ddecayplugin/

正方形包围盒截取算法
1.将被贴花的模型的所有三角形转换到包围盒坐标系内
2.排除三角形3个顶点在包围盒6个面内的三角形,收集3个顶点都在6个面内的三角形, 对于仅有某个顶点在包围盒内,某个顶点不在包围盒内的三角形,使用空间直线和平面交点算法,计算出包围盒平面上的相交点作为新的顶点
3.把收集的顶点创建网格

球形包围盒截取算法
1.跟上面一样
2.只需要判断顶点距离是否小于球形半径即可,对于部分超出球型的顶点,使用空间直线和球型交点的算法, 文章最后有相关算法论文
3.同正方形包围盒

那么Decay 能用在什么地方呢
1.Fps游戏打枪时,墙面的弹坑
2.场景喷漆,比如不规则平面上血渍,颜料等
3.脚印

凡是静态的不规则新纹理贴片,Decay都是最好的选择

对于动态对象,比如蒙皮动画,不要使用Decay,因为Decay要遍历对象的所有三角形,因此实时性能不好。 静态能做Batch。

对于动态对象,建议使用Projector, 它是再次绘制目标一次,然后用新的uv绘制
Projector的动态对象性能极好与 Decay. 但是静态性能极低与Decay,会增加2倍Draw Call, 不能 Combine Batch.
因此要有选择的使用它们。

参考部分
1.光线跟踪的直线与球体求交的快速算法
Link
2.Decals editor ~ part three
Link

Unity3d ddx ddy 法线

最近摸到一个很神奇的API -> DDX, DDY (DX) 或者 FDX, FDY (Opengl)

ddxddy normal

三角面片这么明显的法线有木有

在Nvidia 文档说, ddx/ddy 是求一个近似偏导函数, 以对齐屏幕分辨率的值进行取值

看了好几遍,不懂,然后仔细一想一阶求导不就是斜率嘛, 那么斜率是什么呢,就是平面向量啊。
于是测试了 ddx(worldSpaceVertex.xyz) ddy(worldSpaceVertex.xyz) 可以求出 ddx[(x,x) – (x+1,x)] , ddy[ (y,y) – (y-y)] 即隔壁像素所在三角形和当前像素的斜率差,或者说当前像素到隔壁像素的斜率

x轴 斜率表示前进方向向量, 和 y 轴斜率表示右手方向向量,就可以求 三角面的法线
normalize(ddx) cross normalize(ddy) = normal;
normal = normalize(normal);

这个时候,如果直接输出fixed4(normal,1.0) 就是这样
ddxddy normal fragment

请不要无脑复制转载本文, 由 dreamfairy 原创, 转载请注明出处 本文地址 http://www.dreamfairy.cn/blog/2016/06/15/unity3d-ddx-ddy-normal/
ddx,ddy 只能在 Fragment Shader下使用
Shader如下

			v2f vert (appdata v)
			{
				v2f o;

				float4x4 modelMat = _Object2World;
				float4 worldPos = mul(modelMat, v.vertex);

				o.worldPos = worldPos;
				o.vertex = mul(UNITY_MATRIX_VP, worldPos);
				o.uv = v.uv;

				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				float3 dx = ddx(i.worldPos);
				float3 dy = ddy(i.worldPos);
				float3 normal = normalize(cross(dx, dy));

				normal = -normal * 0.5 + 0.5;
				return fixed4(normal,1.0);
			}

为什么三角形的面片感这么强呢, 因为计算斜率是通过面片计算的, 而传统的法线是通过共面顶点法线进行求平均得出,因此传统的转角处的法线会更加平滑。

有了法线,切线, 副切线,就可以构建切线空间

float3x3 tangleMat = float3x3(normalize(dx), normalize(-dy), normal);
float3 bumpNormal = tex2D(_NormalTex, i.uv);
bumpNormal = bumpNormal * 2 - 1;// range to -1~1 from 0~1
bumpNormal = mul(tangleMat, bumpNormal);
float diff = max(0, dot(i.worldLightDir, bumpNormal));
col *= col * diff;
return fixed4(col,1.0);

有了切线空间,就可以变换凹凸纹理咯。

ddxddy bump

ddx/ddy 还有一个 float2 的参数形式
这个一般是用来 求相邻像素的 uv 值差 ddx(i.uv.xy)。 因此当一个平面和平面平行, 所得值应该就是 1.0/纹理宽(高) 既uv递进值

那么ddx,ddy 可以用在哪里呢。
1. 计算mipmap level 计算当前纹理在屏幕分辨率下的大小是否小于某个mipmap等级的纹理大小,是的话就切换纹理
2. 视差贴图 从视野方向计算像素挤出的位置的uv值
3. and so on…

最后的最后
使用ddx, 和ddy 时, 发现 ddy 的数值在 编辑器模式 和 Game 模式下是相反的, 不知道为什么, 查询了官网也有人有发现这个问题
http://forum.unity3d.com/threads/cg-ddy-function-returning-negated-vector-in-game-view-macro-to-check.407630/

官网的答复是 ddy * _ProjectionParams.x; 然后我查询了 _ProjectionParams.x 文档说 -1 时渲染使用了反矩阵。 好吧,我也是基本没看懂, 不过这么使用后 编辑器和游戏模式的表现都一致了,但是也仅此而已,因为当你生成apk 后, 展示的效果却是 不处理之前的 编辑器里的效果 (我试了 nexus7, motorola me525 等上古神机,还有俺的 华为6p 都是如此)
之后查询 _ProjectionParams 的使用地方, 有提到 UNITY_HALF_TEXEL_OFFSET 下使用, 但是我无论dx, 还是opengl 设备都无法触发这个宏。

这个问题留待以后解决吧:)

最后的最后的最后, 我发现, 在创建切线空间时, x轴表示前进, y 轴表示右侧, z 轴表示朝上 是在 0,0 点以左上角为基准的 (DX), 在Opengl 应以 y 轴为前进,x轴表示右手, 于是最后测试渲染正确。 <- 可能是无解,暂时删除

Unity3d Compute Shader

=。= 准备开始花1个小时探讨这个复杂的 Compute Shader的, 结果从家里拷贝做的 Demo 在女朋友的小霸王笔记本上运行不起来。。只能明天回家在写了。。

就是这个破显卡 🙁 Intel HD Graphics 3000

—–

I’m back…

话说, Compute Shader 能做什么呢,可以把大量的并行计算丢到GPU去计算,因为GPU并行计算的能力爆表
举例来说呢
1.大规模粒子特效,计算粒子的位置,大小,颜色等等(当然传统粒子也可以做到这点,通过传入时间值和带有时间的公式来计算位置,但是Compute Shader 可以实现带有逻辑更复杂的效果, 而且编码更加简单)
2.烟雾,液体的模拟
3.可以做贪食蛇 =.= 像这样
ComputeSnake

请不要无脑复制转载本文, 由 dreamfairy 原创, 转载请注明出处 本文地址 http://www.dreamfairy.cn/blog/2016/06/09/unity3d-compute-shader/

Compute Shader 是 SM5.0 Dx11 的技术,目前只能用在PC上
Unity3D 创就 Compute Shader的位置和创建普通的Shader 在同一个菜单下
一个默认的Compute Shader是这样的

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain

// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> Result;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
	// TODO: insert actual code here!

	Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}

#pragma kernel CSMain 是定义Compute Shader的入口函数名
RWTexture2D Result; 是 Compute Shader 的输入输出纹理 (RW开头,表示 Read And Writhe) 当然 Compute Shader支持自定义构造体,后面说

[numthreads(8,8,1)] 表示,每个线程组创建多少个 Thread, 说到这个就要提到调用 Compute Shader的API了
在CS代码中调用API为 Shader.Dispatch(kernel, r, g, b);
线程组的数量是 r * g * b, 比如 Shader.Dispatch(kernel, 2, 3, 4); 且 [numthreads(8,8,1)] 则线程数为 2 * 3 * 4 = 24 个线程组,每个线程组 8 * 8 * 1 个线程, 总计 24 * 64 = 1536 个线程

不过线程数也是有上线的,微软给了个这么个图表

The allowable parameter values for numthreads depends on the compute shader version.
Compute Shader	Maximum Z	Maximum Threads (X*Y*Z)
cs_4_x	           1	             768
cs_5_0	           64	             1024

然后说说 CSMain 函数的参数 uint3 id : SV_DispatchThreadID
它表示当前运行的线程的id号, id.xyz 表示当前在 numthreads 中的索引

说了辣么多,先来个简单的例子, 用Compute Shader 做 uv 滚动的效果

    private string state = "";
    private const string stateRed = "FillWithRed";
    private const string stateBlack = "FillWithBlack";

RenderTexture rt = new RenderTexture(nTexWidth, nTexWidth, 0);
rt.enableRandomWrite = true;
rt.Create();
int kernel = shader.FindKernel(stateRed);
shader.SetTexture(kernel, "Result", rt);

ChangeState(FillWithRed);

创建 RenderTexture 要记得开启 enableRandomWrite , 否则Shader中无法写入数据
然后就是获取 Shader 中 CSMain函数的指针 int kernel = shader.FindKernel(stateRed);

之后

StartCoroutine(StartGPGPU());
    IEnumerator StartGPGPU() {
        while (true) {
            count = count + changeValue;

            if(count > nTexWidth) {
                ChangeState(state == stateBlack ? stateRed : stateBlack);
            }
            
            shader.Dispatch(kernel, nTexWidth, count,  1);
            
            yield return new WaitForEndOfFrame();
        }
    }

我们用一个携程来做一个uv循环滚动, x 轴不变, y 轴从下往上滚动
nTexWidth 这里定义的数值是 256 。 当Y轴处理的像素超过256后,做一次从0开始涂黑过程。 你可以把线程和像素点一一对应起来, 线程在像素点所在位置进行对应位置涂色操作

Shader 部分

#pragma kernel FillWithRed
#pragma kernel FillWithBlack
RWTexture2D<float4> Result;
[numthreads(1,1,1)]
void FillWithRed (uint3 id : SV_DispatchThreadID)
{
	// TODO: insert actual code here!
	Result[id.xy] = float4(1,0,0,1);
}
[numthreads(1,1,1)]
void FillWithBlack (uint3 id : SV_DispatchThreadID)
{
	// TODO: insert actual code here!
	Result[id.xy] = float4(0,1,0,1);
}

ComputeUVScroll

后面的贪食蛇,粒子,液体后面再说, 先看 LGD vs VG

Unity3D 中的高度雾

fog
图中有2个效果, 分别是 HDR(做法在这里uniy3d-hdr_bloom/, 高度5的白色高度雾
不过除了雾以外,其他都不重要.

unity3d 一直都有一个距离雾,也可以说是深度雾,随着物体的远近慢慢渐变颜色,设置在 Window->Lightning->Fog(Unity 5.x)

这次来实现一个u3d中没有的,从低到高,而不是从远到近的雾效 – 高度雾。

之前做这个效果之前,有查阅些资料,有看到某外国的大大做了个类似的效果,地址在这里 altitude-fog
我们将要做的效果和这个差不多,但是区别在于,老外的做法是需要场景中的每个物体都添加一个雾效材质,这个雾效Shader是判断物件顶点Y轴在世界坐标系中的位置,然后 (FogEnd – 物体Y) / (FogEnd – FogStart) 算出一个在Y轴方向的雾效颜色比例,最后差值一下。

请不要无脑复制转载本文, 由 dreamfairy 原创, 转载请注明出处 本文地址 http://www.dreamfairy.cn/blog/2016/06/06/unity3d-height-fog/
而我采用的办法是通过后期处理来实现高度雾,不需要场景中每个物体单独添加一个雾效材质。 而雾效算法和老外的一样(好吧,其实Linear版的雾效算法全世界都基本一样)。 唯一的难点就是如何在后期处理中的纹理上判断每个像素在世界坐标系中的 Y轴值。

首先,相机部分,肯定要开启输出深度图
Camera.main.depthTextureMode |= DepthTextureMode.Depth;

Shader部分
准备几个参数
_FogStart 雾效在世界坐标系Y轴的开始值
_FogEnd 雾效在世界坐标系Y轴的结束值
_FogColor 雾效颜色

像素着色器雾效的算法

half diff = (_FogEnd – worldPos.y) / (_FogEnd – _FogStart);
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 finalCol = lerp(col, _FogColor, diff);
return finalCol;

到这里都和所有的 Linear雾效做法都一样, 问题就在于如何在 深度图中计算 worldPos.y

一步步来
SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv)
可以获取当前像素的深度值, 值域为 (0~1) 的线性值, 这个值现在还用不了,我们需要真实的深度值
只需要加上这个 LinearEyeDepth 即可
float realDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
但是这个 realDepth 并不是 世界坐标系下被看到物体的真实深度, 仅仅是相机距离被看到物体的真实深度, 不过相机的位置对于Shader来说是已知的不是吗? 就是这个 _WorldSpaceCameraPos
那么要求被看到物体的 worldPos.y 只需要 _WorldSpaceCameraPos + realDepth 即可。 嗯,大致就是如此,不过还有一点点小问题。

当我们把 Camera.Main.transform.Fowrard * realDepth (朝向向量 * 距离 = 到达距离), 所有的深度都被投向垂直于屏幕中央的地方去了,而原本深度是通过相机透视投影生成的近大远小的效果就失去了,于是出现了 距离雾的效果
如图
error_height_fog

最好的情况是,对应每个深度,都刚好有一个朝向它的向量 * 它的深度。
于是你可能会这么想,使用uv 来计算每个像素的朝向,像这样

float2 center = float2(0.5, 0.5);
float2 pixelDir = normalize(i.uv - center);

但是二维向量是没法得知 Z 轴方向的朝向的, 而且随着场景中每个物体远近,相机看它们的朝向,不是uv相减形成的固定角度。

辣么还是老老实实的计算相机的朝向,然后传进Shader吧, 但是我们不需要创建 纹理像素数量那么的相机朝向,使用近似值就能达到很好的效果,剩下交给像素插值就好了, 我们只需要传进 uv在 0,1 ,1.0, 1.1, 1.0 4个角落的向量即可。
因此本质上就是计算相机视锥体的 左上角(上平面 – 右平面), 右上角(上平面 + 有平面), 左下角, 右下角

        Camera cam = Camera.main;
        Transform CamTrans = cam.transform;
        float near = cam.nearClipPlane;
        float far = cam.farClipPlane;
        float halfHeight = cam.nearClipPlane * Mathf.Tan(cam.fieldOfView * 0.5f * Mathf.Deg2Rad);
        Vector3 toRight = CamTrans.right * halfHeight * cam.aspect;
        Vector3 upVector = CamTrans.up * halfHeight;
        Vector3 topVector = CamTrans.forward * near + upVector;
        Vector3 bottomVector = CamTrans.forward * near - upVector;

        Vector3 bottomLeft = bottomVector - toRight; //左下
        Vector3 bottomRight = bottomVector + toRight; //右下
        Vector3 topLeft = topVector - toRight; //左上
        Vector3 topRight = topVector + toRight; //右上

然后在Shader部分,判断uv位置,取对应的向量

                               if (o.uv.y < 0.5 && o.uv.x < 0.5) {
					o.cameraDir = _CameraMat[0];  //左下
				}
				else if (o.uv.y < 0.5 && o.uv.x > 0.5) { //右下
					o.cameraDir = _CameraMat[1];
				}
				else if (o.uv.y > 0.5 && o.uv.x < 0.5) { //左上
					o.cameraDir = _CameraMat[2];
				}
				else if (o.uv.y > 0.5 && o.uv.y > 0.5) { //右上
					o.cameraDir = _CameraMat[3];
				}

最后在 fragment 中计算 worldPos.y即可

float3 worldPos = _WorldSpaceCameraPos + realDepth * i.cameraDir.xyz;

接下来运行一下,就可以出现本文章第一张图那样的效果啦。