CapsuleAO实现的学习
正是一个炎夏,又到了整活的好时候。最近抽些时间研究下CapsuleAO,记述实践体会。
1.简介
这是一个通过在角色骨骼上绑定虚拟胶囊体并以数学方法实现胶囊近似的AO环境光遮蔽效果的方法,
当角色处于阴影中时,CapsuleAO的效果比较明显。当角色在露天环境中,效果较弱。
下图是我自己游戏里截图的效果,以做参考:
不同项目有不同的实现,UE4中也有类似实现,叫做Capsule Shadow,这里不多做介绍:
2.CapsuleAO实现尝试
首先用自己的思路实现一下,首先参考了IQ大神的SphereAO:
https://www.shadertoy.com/view/4djSDy
抛开公式的话,其实就是一个点光源的做法,然后把颜色改成黑色加上函数系数进行调节,使其更接近AO的感觉。
这是尝试实现的效果:
实现时要将球体变为胶囊,要在胶囊的长度轴上计算投影。投影完后对投影长度进行Clamp约束,约束后的两个端点
和周围像素进行Distance计算,这样直接就是胶囊的效果了,而不是圆柱。做了个简单的图:
具体见代码。
Shader:
Shader "Unlit/CapsuleAOShader"
{
Properties
{
_Adjust("Adjust", float) = 2
_CapsuleRadius("Capsule Radius", float) = 0.3 _DistanceFix("Distance Fix", float) = 0.3
} SubShader
{
Tags { "RenderType"="Opaque" } Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag #include "UnityCG.cginc" struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
}; struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION; float3 wPos : TEXCOORD2;
}; float4 _PlanePos;
float4 _PlaneNormal;
float4 _CapsuleP0;
float4 _CapsuleP1;
float _Adjust;
float _CapsuleRadius; float _DistanceFix; float3 Formula(float3 sphP0, float3 sphP1, float length, float radius, float3 comparePoint)
{
float3 norm1 = normalize(sphP1 - sphP0);
float3 relativeComparePoint = comparePoint-sphP0;
float3 projValue = dot(relativeComparePoint, norm1); float x = clamp(projValue, -length, length);
float3 projPoint = sphP0 + x * norm1;
float3 norm2 = normalize(comparePoint - projPoint); return projPoint + norm2 * radius;
} float3 DistanceFix(float3 distancePoint, float wPos, float3 norm, float distanceFix)
{
return distancePoint + norm * distanceFix;
} float Occlusion(float3 pos, float3 nor, float3 sphP0, float3 sphP1)
{
float3 finalPoint = Formula(sphP0, sphP1, distance(sphP1,sphP0), _CapsuleRadius, pos);/*Add*/
finalPoint = DistanceFix(finalPoint, pos, nor, _DistanceFix);/*Add*/
float3 di = finalPoint - pos;
float l = length(di);
float nl = dot(nor, di / l);
float h = l / 0.5;
float h2 = h * h;
float k2 = 1.0 - h2 * nl*nl; float res = pow(clamp(0.5*(nl*h + 1.0) / h2, 0.0, 1.0), 1.5); return res;
} v2f vert (appdata v)
{
v2f o = (v2f)0;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv; o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o;
} fixed4 frag (v2f i) : SV_Target
{
float occ0 = Occlusion(i.wPos, _PlaneNormal.xyz, _CapsuleP0.xyz, _CapsuleP1.xyz);
return 1.0 - occ0;
}
ENDCG
}
}
}
c#部分,控制胶囊的传入,与球体不同;胶囊需要一个长度向量信息表示长度轴的朝向:
using System.Collections;
using System.Collections.Generic;
using UnityEngine; public class CapsuleAOParamUpdate : MonoBehaviour
{
public Transform capsuleP0;
public Transform capsuleP1;
Material mMaterial; private void Update()
{
mMaterial = GetComponent<MeshRenderer>().sharedMaterial; mMaterial.SetVector("_PlanePos", transform.position);
mMaterial.SetVector("_PlaneNormal", transform.up); mMaterial.SetVector("_CapsuleP0", new Vector4(capsuleP0.position.x, capsuleP0.position.y, capsuleP0.position.z, 1f));
mMaterial.SetVector("_CapsuleP1", new Vector4(capsuleP1.position.x, capsuleP1.position.y, capsuleP1.position.z, 1f));
}
}
3.Capsule Shadow实现尝试
看了下UE里的实现代码,比较复杂。这部分自己处理比较简单。
先看最终效果(支持非平面表面,光照方向改变,但某些光照角度有一定穿帮感):
首先,以主光平行光的方向作为投影平面,在shader的frag里得到地面每个像素世界坐标时,
将像素位置,胶囊位置,胶囊方向向量等都投影到平面上进行计算。
大致如下图:
胶囊体需要两个参数,以确定胶囊方向。根据投影后的胶囊方向和地面法线位置,得到叉乘位置,然后可以作为x,y坐标采样贴图。
采样贴图虽然效果好些,但开销较高,所以也可以自己去拟合。
接着混合3个权重信息:
- 对平行光的相反方向做一个点乘处理,防止反方向上也被映射上阴影(FadeDirectionWeight)。
- 对主要投影区域做一个权重系数,让只有被投影地面上有阴影图案(MaskWeight)。
- 将投影前的地面世界空间坐标和投影前的胶囊位置做一个权重,让阴影有一个深到浅的渐变效果(FadeWeight)。
最后将所有权重混合:
return shadowCol * maskWeight * fadeWeight * faceDirectionWeight;
最后上代码,代码为采样贴图版本,开销比较高。
shader部分:
Shader "Unlit/CapsuleShadowShader"
{
Properties
{
_MainShapeTex ("Main Shape Tex (RGB)", 2D) = "white" {}
_UvOffset("Uv Offset", vector) = (0.0, 0.0, 0.0, 0.0)
_ShadowScaleFactor("Shadow Scale Factor", float) = 1.0
_FadeWeightFactor("Fade Weight Factor", float) = 10.0
_FadeDirectionFactor("Fade Direction Factor", float) = 1.0
} SubShader
{
Tags { "RenderType"="Opaque" } Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag #include "UnityCG.cginc" struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
}; struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION; float3 wPos : TEXCOORD2;
}; uniform float4 _LightDirectionVector;
uniform float4 _CapsulePos1;
uniform float4 _CapsulePos2;
uniform float4 _CapsulePos3;
uniform float4 _CapsulePos4;
uniform float4 _CapsulePos5;
uniform float4 _CapsulePos6;
uniform float4 _CapsulePos7;
uniform float4 _CapsulePos8;
uniform float4 _CapsulePos9;
uniform float4 _CapsulePos10;
uniform float4 _CapsulePos11;
uniform float4 _CapsulePos12;
uniform float4 _CapsulePos13;
uniform float4 _CapsulePos14; sampler2D _MainShapeTex;
float4 _UvOffset;
float _ShadowScaleFactor;
float _FadeWeightFactor; float _FadeDirectionFactor; v2f vert (appdata v)
{
v2f o = (v2f)0;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv; o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o;
} half3 ProjectOnPlane(half3 vec, half3 planeNormal)
{
float num = dot(planeNormal, planeNormal);
float num2 = dot(vec, planeNormal);
return half3(vec.x - planeNormal.x * num2 / num, vec.y - planeNormal.y * num2 / num, vec.z - planeNormal.z * num2 / num);
} half CapsuleShadow(float3 capsulePos, float3 capsulePosDirectVec, float3 wPos)
{
half faceDirectionWeight = max(_FadeDirectionFactor, dot(normalize(capsulePos - wPos), _WorldSpaceLightPos0.xyz)); half fadeDistance = distance(wPos, capsulePos);
half fadeWeight = (1.0/distance(wPos, capsulePos)) * _FadeWeightFactor; half3 proj = ProjectOnPlane(wPos, _WorldSpaceLightPos0.xyz);
half3 centerProj = ProjectOnPlane(capsulePos, _WorldSpaceLightPos0.xyz);
half3 centerVectorProj = ProjectOnPlane(capsulePosDirectVec, _WorldSpaceLightPos0.xyz);
half3 dir1 = normalize(centerVectorProj - centerProj);
half3 dir2 = normalize(cross(dir1, _WorldSpaceLightPos0.xyz)); half x = dot(proj - centerProj, dir1) * _UvOffset.z + _UvOffset.x;
half y = dot(proj - centerProj, dir2) * _UvOffset.w + _UvOffset.y;
half shadowCol = tex2D(_MainShapeTex, half2(x, y)).r; half maskWeight = saturate(distance(proj, centerProj) / _ShadowScaleFactor);
maskWeight = max(0.4, maskWeight); return shadowCol * maskWeight * fadeWeight * faceDirectionWeight;
} fixed4 frag (v2f i) : SV_Target
{
half col = CapsuleShadow(_CapsulePos1, _LightDirectionVector, i.wPos);
col = max(col, CapsuleShadow(_CapsulePos2, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos3, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos4, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos5, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos6, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos7, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos8, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos9, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos10, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos11, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos12, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos13, _LightDirectionVector, i.wPos));
col = max(col, CapsuleShadow(_CapsulePos14, _LightDirectionVector, i.wPos)); return lerp(0.5, 0.0, col);
}
ENDCG
}
}
}
c#部分:
using System.Collections;
using System.Collections.Generic;
using UnityEngine; public class CapsuleAOParamUpdate : MonoBehaviour
{
public Transform capsuleHead;
public Transform capsuleBody1;
public Transform capsuleBody2;
public Transform capsuleBody3; public Transform capsuleLeftArm1;
public Transform capsuleLeftArm2;
public Transform capsuleLeftArm3;
public Transform capsuleRightArm1;
public Transform capsuleRightArm2;
public Transform capsuleRightArm3;
public Transform capsuleLeftLeg1;
public Transform capsuleLeftLeg2;
public Transform capsuleLeftLeg3;
public Transform capsuleRightLeg1;
public Transform capsuleRightLeg2;
public Transform capsuleRightLeg3; public Transform lightDirectionVector; private Material mMaterial; private void Update()
{
mMaterial = GetComponent<MeshRenderer>().sharedMaterial; if (capsuleHead)
mMaterial.SetVector("_CapsulePos1", capsuleHead.position); if (capsuleBody1)
mMaterial.SetVector("_CapsulePos2", capsuleBody1.position);
if (capsuleBody2)
mMaterial.SetVector("_CapsulePos2", capsuleBody2.position);
if (capsuleBody3)
mMaterial.SetVector("_CapsulePos2", capsuleBody3.position); if (capsuleLeftArm1)
mMaterial.SetVector("_CapsulePos3", capsuleLeftArm1.position);
if (capsuleLeftArm2)
mMaterial.SetVector("_CapsulePos4", capsuleLeftArm2.position);
if (capsuleLeftArm3)
mMaterial.SetVector("_CapsulePos5", capsuleLeftArm3.position); if (capsuleRightArm1)
mMaterial.SetVector("_CapsulePos6", capsuleRightArm1.position);
if (capsuleRightArm2)
mMaterial.SetVector("_CapsulePos7", capsuleRightArm2.position);
if (capsuleRightArm3)
mMaterial.SetVector("_CapsulePos8", capsuleRightArm3.position); if (capsuleLeftLeg1)
mMaterial.SetVector("_CapsulePos9", capsuleLeftLeg1.position);
if (capsuleLeftLeg2)
mMaterial.SetVector("_CapsulePos10", capsuleLeftLeg2.position);
if (capsuleLeftLeg3)
mMaterial.SetVector("_CapsulePos11", capsuleLeftLeg3.position); if (capsuleRightLeg1)
mMaterial.SetVector("_CapsulePos12", capsuleRightLeg1.position);
if (capsuleRightLeg2)
mMaterial.SetVector("_CapsulePos13", capsuleRightLeg2.position);
if (capsuleRightLeg3)
mMaterial.SetVector("_CapsulePos14", capsuleRightLeg3.position); mMaterial.SetVector("_LightDirectionVector", lightDirectionVector.position);
}
}
4.总结
这篇文章以学习为主,就不提供下载工程了。具体使用还需自行开发。
若该方案需要在项目中的落地,我做如下建议:
1.不要一次性在shader里传入所有胶囊,可以分成多个pass来做,也可以放到屏幕blit里去做,或者先画到一张临时RT里。
2.当角色暴露在强光下,基本看不出CapsuleAO效果,当角色在阴影中或处于柔和光照环境下,才会有明显的CapsuleAO表现。
3.也可以只有主角有CapsuleAO效果。
4.也可以CapsuleAO+CapsuleShadow,但是要分成多个pass来做。
CapsuleAO实现的学习的更多相关文章
- 从直播编程到直播教育:LiveEdu.tv开启多元化的在线学习直播时代
2015年9月,一个叫Livecoding.tv的网站在互联网上引起了编程界的注意.缘于Pingwest品玩的一位编辑在上网时无意中发现了这个网站,并写了一篇文章<一个比直播睡觉更奇怪的网站:直 ...
- Angular2学习笔记(1)
Angular2学习笔记(1) 1. 写在前面 之前基于Electron写过一个Markdown编辑器.就其功能而言,主要功能已经实现,一些小的不影响使用的功能由于时间关系还没有完成:但就代码而言,之 ...
- ABP入门系列(1)——学习Abp框架之实操演练
作为.Net工地搬砖长工一名,一直致力于挖坑(Bug)填坑(Debug),但技术却不见长进.也曾热情于新技术的学习,憧憬过成为技术大拿.从前端到后端,从bootstrap到javascript,从py ...
- 消息队列——RabbitMQ学习笔记
消息队列--RabbitMQ学习笔记 1. 写在前面 昨天简单学习了一个消息队列项目--RabbitMQ,今天趁热打铁,将学到的东西记录下来. 学习的资料主要是官网给出的6个基本的消息发送/接收模型, ...
- js学习笔记:webpack基础入门(一)
之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...
- Unity3d学习 制作地形
这周学习了如何在unity中制作地形,就是在一个Terrain的对象上盖几座小山,在山底种几棵树,那就讲一下如何完成上述内容. 1.在新键得项目的游戏的Hierarchy目录中新键一个Terrain对 ...
- 《Django By Example》第四章 中文 翻译 (个人学习,渣翻)
书籍出处:https://www.packtpub.com/web-development/django-example 原作者:Antonio Melé (译者注:祝大家新年快乐,这次带来<D ...
- 菜鸟Python学习笔记第一天:关于一些函数库的使用
2017年1月3日 星期二 大一学习一门新的计算机语言真的很难,有时候连函数拼写出错查错都能查半天,没办法,谁让我英语太渣. 关于计算机语言的学习我想还是从C语言学习开始为好,Python有很多语言的 ...
- 多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类)
前言:刚学习了一段机器学习,最近需要重构一个java项目,又赶过来看java.大多是线程代码,没办法,那时候总觉得多线程是个很难的部分很少用到,所以一直没下决定去啃,那些年留下的坑,总是得自己跳进去填 ...
随机推荐
- Java方法区的理解
方法区逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩. 但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap,目的就是要和堆分开 所以方法区看作是一块 ...
- [bug] Job for network.service failed because the control process exited with error code
原因 复制虚拟机,没有改网卡配置文件 参考 https://blog.csdn.net/dongfei2033/article/details/81124465
- 电脑无法开机,用一个U盘先备份C盘部分重要文件并重装Win10系统的教程?
电脑无法开机,用一个U盘先备份C盘部分重要文件并重装Win10系统的教程.? 这应该是修电脑的万能方法,重装系统能解决绝大多数非硬件导致的电脑故障,但之前要备份一下桌面的一些个人文件.所以想学一下,以 ...
- kotlin中的嵌套类与内部类
Java中的内部类和静态内部类在Java中内部类简言之就是在一个类的内部定义的另一个类.当然在如果这个内部类被static修饰符修饰,那就是一个静态内部类.关于内部类 和静态内部类除了修饰符的区别之外 ...
- 在react中使用redux并实现计数器案例
React + Redux 在recat中不使用redux 时遇到的问题 在react中组件通信的数据是单向的,顶层组件可以通过props属性向下层组件传递数据,而下层组件不能向上层组件传递数据,要实 ...
- Navicat注册机报错No all pattern found! file already patched
第一步:先把注册机放入安装目录. (这一步非常关键,先不要打开桌面上安装好的快捷方式!!) 第二步:如果之前下载过,把注册表清理干净 计算机\HKEY_CURRENT_USER\SOFTWARE\Pr ...
- Proteus中包含的主流单片机列举
经常使用Proteus的朋友面临的一个问题就是,这个设计用Proteus能仿真吗?在初级阶段,我们仅仅会参考Proteus是否有对应的器件以及器件是否有仿真模型来决断这个问题.有就能仿真,没有就不能仿 ...
- TVM性能评估分析(六)
TVM性能评估分析(六) Figure 1. The workflow of development PC, compile, deploy to the device, test, then mo ...
- ApplicationListener接口,在spring容器初始化后执行的方法
一.如果我们希望在Spring容器将所有的Bean都初始化完成之后,做一些操作,那么就可以使用ApplicationListener接口,实现ApplicationListener接口中的onAppl ...
- Druid数据库连接池基本使用
一.导入Druid的jar包和数据库驱动jar包 二.定义配置文件 与c3p0不同,Druid的配置文件是properties形式的.而且Druid不像c3p0那样可以自动加载配置文件,Druid需要 ...