次表面散射指的是光线射入半透明材质,在内部发生散射后再透射出来的光线传播过程,考虑到有些项目会需要使用次表面散射,下面就给大家介绍下在Unity3D中次表面散射的简单实现,希望可以帮到大家。

一、前言

本文旨在与大家一起探讨学习新知识,如有疏漏或者谬误,请大家不吝指出。

以下内容参考了GPU精粹1中关于次表面散射一节的内容。

二、概述

次表面散射,英文全称为Subsurface Scattering,简称SSS。指的是光线射入半透明材质,在内部发生散射后再透射出来的光线传播过程。真实世界中拥有次表面散射的材质有蜡烛、大理石、玉石以及人的皮肤等。要模拟物理上真实的次表面散射是很复杂的一件事,比较经典的次表面散射模型有BSSRDF,全称叫双向次表面散射反射分布函数。本文中并未使用BSSRDF模型,而是简单的使用了exp指数函数对深度进行计算以此模拟散射。

以下为实现后的最终效果:

三、原理

首先我们来对次表面散射物体的光照做一个分解,也算是一个简单的建模过程。

Color = Diffuse * Scattering + SpecularColor;

Diffuse指的是物体表面的漫反射颜色,Scattering是散射的颜色,SpecularColor是高光颜色。

1、漫反射的计算

漫反射的计算公式,在这里我们使用的是环绕光照计算公式,即:

1
diff = ( dot(normal, lightDir) + wrap )/( 1+wrap )

其中normal为顶点的法线,lightDir是光照方向,wrap为环绕参数。传统的Lambert光照中,当物体表面的法线与光源方向垂直的时候,其产生的光照结果为0,但是在次表面散射物体中,由于内部散射光线的传播,导致其在上述情形下光照结果不会完全为0。所以为了减少传统Lambert光照中的黑暗区域,我们使用环绕光照公式,当dot(normal, lightDir)结果为0的时候,我们强制其至少有一点点亮度,即wrap/(1+wrap)。

2、散射的计算

我们假设物体表面的散射是均匀分布的,并且无视光源位置以及光照方向对散射的影响,取物体在视线方向上的深度值作为参数,带入exp指数函数中进行计算。当然上述假设并不符合物理规律,但是考虑到效果以及效率的问题,我们只好先这么干了。

为了获取物体在视线方向上的深度值,我们需要先以cull front模式渲染一遍物体,保存物体背面的顶点的深度值信息,然后再回到正常的cull back模式下渲染物体,使用(backDepth-frontDepth)来求出深度值,最后带入公式exp(-C*depth)中。C为外部传入的参数,用于调节物体的透光率。

3、高光的计算

高光的计算我们使用经典的BlinnPhong光照公式,即:

1
2
Specular = pow((dot(normal, half)), shiness);
Half = normalize(lightDir + viewDir);

其中normal为顶点法线向量,half为半角向量,是入射光向量与视点向量的角平分线向量,shiness为高光指数。

BlinnPhong光照模型相比较于Phong光照模型,其高光区域更平滑柔和,这也是为什么我们使用它。

4、半透明

由于在散射计算中,需要使用到物体表面顶点的深度值信息,导致我们在渲染时不 能关闭ZWrite,这就使得我们不能通过Unity3D中设置RenderType=Transparent、Queue=Transparent来实现半透明混合效果。在Unity3D中,要实现半透明,一般的做法是:

1
2
3
Tags { "RenderType"="Transparent" "Queue"="Transparent"}
Blend SrcAlpha OneMinusSrcAlpha
ZWrite off

所以,我们需要寻找另外一种方法来实现半透明,通过GrabPass操作来获取除物体本身以外的屏幕渲染结果,然后我们在片段着色器中手动进行混合计算,以此达到半透明效果。当然需要注意的是,GrabPass本身的操作比Alpha混合要昂贵的多,需要牺牲更多的计算性能,另外GrabPass在某些手机平台上可能不被支持。

四、实现

下面给出顶点着色器以及片段着色器的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
struct appdata
{
       float4 vertex : POSITION;
       float2 uv : TEXCOORD0;
       float3 normal:NORMAL;
};
  
struct v2f
{
       float2 uv : TEXCOORD0;
       float4 screenUV:TEXCOORD1;
       float3 lightDir:TEXCOORD2;
       float3 viewDir:TEXCOORD3;
       float3 normal:TEXCOORD4;
       float4 grabUV:TEXCOORD5;
       UNITY_FOG_COORDS(1)
       float4 vertex : SV_POSITION;
};
  
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _GrabTexture;
sampler2D _BackDepthTex;
float4 _AttenuationC;
float4 _Color;
float _Shininess;
float _ScatteringFactor;
float _Wrap;
  
struct LightingInput {
       float3 Albedo;
       float3 Normal;
       float Gloss;
       float Specular;
       float Alpha;
};
  
float4 CalculateLighting (LightingInput i, float3 lightDir, float3 viewDir, float atten, float3 scattering)
{
       float3 h = normalize (lightDir + viewDir);
       
       float diff = (dot (i.Normal, lightDir)+_Wrap)/(1+_Wrap);
       diff = saturate (diff);
       
       float nh = (dot (i.Normal, h)+_Wrap)/(1+_Wrap);
       nh = saturate(nh);
       float spec = pow (nh, i.Specular*128.0) * i.Gloss;
       
       float4 c;
       c.rgb = (i.Albedo * _LightColor0.rgb * diff *scattering + _LightColor0.rgb * _SpecColor.rgb * spec) * (atten * 2);
       c.a = i.Alpha + _SpecColor.a * spec * atten;
 
       return c;
}
  
v2f vert (appdata v)
{
       v2f o;
       o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
       o.uv = TRANSFORM_TEX(v.uv, _MainTex);
       o.screenUV = ComputeScreenPos(o.vertex);
       o.lightDir = ObjSpaceLightDir(v.vertex);
       o.viewDir = ObjSpaceViewDir(v.vertex);
       o.normal = v.normal;
       o.grabUV = ComputeGrabScreenPos(o.vertex);
       UNITY_TRANSFER_FOG(o,o.vertex);
       return o;
}
  
float4 frag (v2f i) : SV_Target
{
       // sample the texture
       float4 col = tex2D(_MainTex, i.uv);
       //
       float frontDepth = LinearEyeDepth( i.screenUV.z/i.screenUV.w );
       //
       float2 backDepthUV = i.screenUV.xy/i.screenUV.w;
       float4 backDepthColor = tex2D(_BackDepthTex, backDepthUV);
       float backDepth = LinearEyeDepth(DecodeFloatRGBA(backDepthColor));
       //do scattering
       float depth = backDepth-frontDepth;
       float3 scattering = exp(-_AttenuationC.xyz*depth);
       //do lighting
       LightingInput lightVar;
       lightVar.Albedo = col.rgb * _Color.rgb;
       lightVar.Gloss = col.a;
       lightVar.Alpha = col.a * _Color.a;
       lightVar.Specular = _Shininess;
       lightVar.Normal = i.normal;
 
       col = CalculateLighting (lightVar, i.lightDir, i.viewDir, _LightColor0.a, scattering);
       //blend
       //col.xyz = col.a*col.rgb + (1-col.a)*tex2D(_GrabTexture, i.grabUV.xy/i.grabUV.w);
       col.xyz = lerp(tex2D(_GrabTexture, i.grabUV.xy/i.grabUV.w), col.rgb, col.a);
       // apply fog
       UNITY_APPLY_FOG(i.fogCoord, col);
       return col;
}

照例,我们对上述代码中某些函数进行说明:

1、ComputeScreenPos函数是将经过透视投影的顶点变换到屏幕坐标系中,然后就可以使用xy/w的值作为UV取屏幕坐标系下的深度图的值。

具体细节可以参看UnityCG.cginc文件,这里也将代码贴出来:

1
2
3
4
5
6
7
8
9
10
11
inline float4 ComputeScreenPos (float4 pos) {
       float4 o = pos * 0.5f;
       #if defined(UNITY_HALF_TEXEL_OFFSET)
       o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w * _ScreenParams.zw;
       #else
       o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
       #endif
       
       o.zw = pos.zw;
       return o;
}

2、ComputeGrabScreenPos函数做的事情跟上述ComputeScreenPos函数是一样的,只不过对于GrabPass取到的渲染结果与屏幕空间不太一致,这里也列出代码:

1
2
3
4
5
6
7
8
9
10
11
inline float4 ComputeGrabScreenPos (float4 pos) {
       #if UNITY_UV_STARTS_AT_TOP
       float scale = -1.0;
       #else
       float scale = 1.0;
       #endif
       float4 o = pos * 0.5f;
       o.xy = float2(o.x, o.y*scale) + o.w;
       o.zw = pos.zw;
       return o;
}

3、LinearEyeDepth函数是将经过透视投影变换的深度值还原成其在View坐标系中的值。具体细节读者可以参考此链接:

http://blog.sina.com.cn/s/blog_70f96aa90102v0wd.html

这篇博客是本人早期写的,大体意思有说明到,如果要穷究细节,则需要对透视投影做深入了解了。

4、DecodeFloatRGBA与EncodeFloatRGBA是一对函数。EncodeFloatRGBA用于将float值编码到RGBA四个通道上,Decode则是相应的解码过程。这两个函数是为了提高深度值的精度,以便于进行深度值计算时不会产生太大误差。

5、_BackDepthTex是物体背面的深度图,由Camera.RenderWithShader()产生,这部分的代码在脚本中实现,读者可以参考完整示例。

下面给出完整示例程序:

 张明 2020-02-25 23楼
源码在GAD改版后丢失了,这里放百度网盘,需要的自取:链接:https://pan.baidu.com/s/1LBPycNqmlsmFFbLWzvkA7Q 提取码:tqar

Unity3D教程:次表面散射的简单实现的更多相关文章

  1. Unity3d 屏幕空间人体皮肤知觉渲染&次表面散射Screen-Space Perceptual Rendering & Subsurface Scattering of Human Skin

    之前的人皮渲染相关 前篇1:unity3d Human skin real time rendering 真实模拟人皮实时渲染 前篇2:unity3d Human skin real time ren ...

  2. Unity3d shader之次表面散射(Subsurface Scattering)

    次表面散射是一种非常常用的效果,可以用在很多材质上如皮肤,牛奶,奶油奶酪,番茄酱,土豆等等  初衷是想做一个牛奶shader的,但后来就干脆研究了sss这是在vray上的次表面散射效果 这是本文在un ...

  3. (转)【风宇冲】Unity3D教程宝典之Blur

    原创文章如需转载请注明:转载自风宇冲Unity3D教程学院                   BlurBlur模糊其实理解了以后非常简单.核心原理就是 1个点的颜色 并不用该点的颜色,而是用该点周围 ...

  4. (转)【风宇冲】Unity3D教程宝典之AssetBundles:第一讲

    自:http://blog.sina.com.cn/s/blog_471132920101gz8z.html 原创文章如需转载请注明:转载自风宇冲Unity3D教程学院                 ...

  5. (转)GEM -次表面散射的实时近似

    次表面散射(Subsurface Scattering),简称SSS,或3S,是光射入非金属材质后在内部发生散射, 最后射出物体并进入视野中产生的现象, 即光从表面进入物体经过内部散射,然后又通过物体 ...

  6. Unity3D教程宝典之Web服务器篇:(第三讲)PHP的Hello World

    转载自风宇冲Unity3D教程学院 引言:PHP是比较简单的编程语言,即使没接触过的也可以现学现用.PHP教程文档PHP100视频教程                           Unity接 ...

  7. Unity3D教程宝典之Web服务器篇:(第二讲)从服务器下载图片

    转载自风宇冲Unity3D教程学院                                    从Web服务器下载图片 上一讲风宇冲介绍了wamp服务器及安装.这回介绍如何从服务器下载内容至 ...

  8. Unity3D教程:无缝地形场景切换的解决方法

    http://www.unitymanual.com/6718.html 当我们开发一个大型项目的时候-会遇到这样的问题(地形场景的切换)这个只是字面意思-并不是重场景1的100  100 100坐标 ...

  9. 【教程】新手如何制作简单MAD和AMV,学不会那都是时辰

    [教程]新手如何制作简单MAD和AMV,学不会那都是时 http://tieba.baidu.com/p/2303522172 [菜鸟教你做MAD]Vegas制作MAD入门教程 http://tieb ...

  10. (转)【风宇冲】Unity3D教程宝典之AssetBundles:第二讲

    原创文章如需转载请注明:转载自风宇冲Unity3D教程学院                             AssetBundles第二讲:AssetBundles与脚本 所有Unity的As ...

随机推荐

  1. Swagger介绍和应用

    1.什么是swaggerSwagger是一个规范和完整的框架,用于生成.描述.调用和可视化RESTful风格的Web服务.简单来说,Swagger是一个功能强大的接口管理工具,并且提供了多种编程语言的 ...

  2. codeblocks快捷键注释

    ctrl+shift+c可以快速注释掉多行. ctrl+shift+x可以取消注释

  3. linux:权限管理

    权限概述 linux一般讲文件可存 / 取 访问的身份分为3个类别:owner.group.others,且3种身份各有 read.write.execute等权限 权限介绍 在多用户计算机系统中,权 ...

  4. ffmpeg简易播放器(4)--使用SDL播放音频

    SDL(英语:Simple DirectMedia Layer)是一套开放源代码的跨平台多媒体开发函数库,使用C语言写成.SDL提供了数种控制图像.声音.输出入的函数,让开发者只要用相同或是相似的代码 ...

  5. Vue项目实战:构建你的第一个项目

    Vue项目实战:从零到一构建你的第一个应用 准备工作 在开始使用Vue之前,请确保您已经安装了Node.js 16.0或更高版本.Node.js是运行Vue项目所必需的JavaScript运行环境. ...

  6. [Ynoi2015] 我回来了 题解

    \(NOIP\) 考前祈福. 实际上,每种伤害 \(d\) 打出的亵渎次数可以转化为: \[1+\max\limits_{i=0}^{\lceil\frac{n}{d}\rceil}(i[\sum\l ...

  7. Android Service后台服务进程意外被kill掉之后如何重启

    Service组件在android开发中经常用到,经常作为后台服务,需要一直保持运行,负责处理一些不必展示的任务.而一些安全软件,会有结束进程的功能,如果不做Service的保持,就会被其杀掉. 那么 ...

  8. 基于Unity调取摄像头方式的定时抓拍保存图像方法小结

    上一篇<Maxmspjitter实现实时抓取摄像头画面并制成序列图 (定时抓拍)>已讲到了定时抓拍的相关问题解决方案,这一篇继续,采用不同的方法,不同的平台----基于Unity. 目标明 ...

  9. 他来了,为大模型量身定制的响应式编程范式(1) —— 从接入 DeepSeek 开始吧

    哒哒哒,他来了! 今天我们要介绍一种新型的 Java 响应式大模型编程范式 -- FEL.你可能听说过 langchain,那么你暂且可以把 FEL 看作是 Java 版本的 langchain. 话 ...

  10. 读论文-新闻推荐系统:近期进展、挑战与机遇的评述(News recommender system_ a review of recent progress, challenges, and opportunities)

    前言 今天读的论文为一篇于2022年发表在"人工智能评论"(Artificial Intelligence Review)的论文,文章主要强调了NRS面临的主要挑战,并从现有技术中 ...