正是一个炎夏,又到了整活的好时候。最近抽些时间研究下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个权重信息:

  1. 对平行光的相反方向做一个点乘处理,防止反方向上也被映射上阴影(FadeDirectionWeight)。
  2. 对主要投影区域做一个权重系数,让只有被投影地面上有阴影图案(MaskWeight)。
  3. 将投影前的地面世界空间坐标和投影前的胶囊位置做一个权重,让阴影有一个深到浅的渐变效果(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实现的学习的更多相关文章

  1. 从直播编程到直播教育:LiveEdu.tv开启多元化的在线学习直播时代

    2015年9月,一个叫Livecoding.tv的网站在互联网上引起了编程界的注意.缘于Pingwest品玩的一位编辑在上网时无意中发现了这个网站,并写了一篇文章<一个比直播睡觉更奇怪的网站:直 ...

  2. Angular2学习笔记(1)

    Angular2学习笔记(1) 1. 写在前面 之前基于Electron写过一个Markdown编辑器.就其功能而言,主要功能已经实现,一些小的不影响使用的功能由于时间关系还没有完成:但就代码而言,之 ...

  3. ABP入门系列(1)——学习Abp框架之实操演练

    作为.Net工地搬砖长工一名,一直致力于挖坑(Bug)填坑(Debug),但技术却不见长进.也曾热情于新技术的学习,憧憬过成为技术大拿.从前端到后端,从bootstrap到javascript,从py ...

  4. 消息队列——RabbitMQ学习笔记

    消息队列--RabbitMQ学习笔记 1. 写在前面 昨天简单学习了一个消息队列项目--RabbitMQ,今天趁热打铁,将学到的东西记录下来. 学习的资料主要是官网给出的6个基本的消息发送/接收模型, ...

  5. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  6. Unity3d学习 制作地形

    这周学习了如何在unity中制作地形,就是在一个Terrain的对象上盖几座小山,在山底种几棵树,那就讲一下如何完成上述内容. 1.在新键得项目的游戏的Hierarchy目录中新键一个Terrain对 ...

  7. 《Django By Example》第四章 中文 翻译 (个人学习,渣翻)

    书籍出处:https://www.packtpub.com/web-development/django-example 原作者:Antonio Melé (译者注:祝大家新年快乐,这次带来<D ...

  8. 菜鸟Python学习笔记第一天:关于一些函数库的使用

    2017年1月3日 星期二 大一学习一门新的计算机语言真的很难,有时候连函数拼写出错查错都能查半天,没办法,谁让我英语太渣. 关于计算机语言的学习我想还是从C语言学习开始为好,Python有很多语言的 ...

  9. 多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类)

    前言:刚学习了一段机器学习,最近需要重构一个java项目,又赶过来看java.大多是线程代码,没办法,那时候总觉得多线程是个很难的部分很少用到,所以一直没下决定去啃,那些年留下的坑,总是得自己跳进去填 ...

随机推荐

  1. C# 搞桌面UI适配国产麒麟Linux+龙芯遇到的一些坑

    由于一些国企有国产化的需求,所以搞了C#适配银河麒麟,适配了X64和龙芯MIPS版本 1. 在银河麒麟的龙芯版本中 pipe2 不能使用,x64版本上却可以用.  pipe2 用来做自定义消息的,搞U ...

  2. ES6学习-0 前言

    本菜鸟做了二十来年的程序开发了,前后台都写过,队伍也带过.大约是2000年左右,是用dephi 写后台CGI,所有的html,js,css基本都是混在CGI里输出到前台的,那时也没有明确的前后台的概念 ...

  3. Java中日志组件详解

    avalon-logkit Java中日志组件详解 lanhy 发布于 2020-9-1 11:35 224浏览 0收藏 作为开发人员,我相信您对日志记录工具并不陌生. Java还具有功能强大且功能强 ...

  4. tar解压某个目录 tar解压某个指定的文件或者文件夹

    tar解压某个目录 tar解压某个指定的文件或者文件夹 发布时间:2017-05-30 来源:服务器之家   1. 先查看压缩文档中有那些文件,如果都不清楚文件内容,然后就直接解压,这个是不可能的 使 ...

  5. 使用GitHub绑定域名免费创建自己的博客

    通过GitHub创建一个免费的.有上传网页功能的.可以绑定域名的个人博客,或者网站. 在这之前的必要条件: 1.有自己的域名,能添加CNAME或者A记录 2.注册好了GitHub账号 方法: 1.添加 ...

  6. who -b

    ~]# who -b 系统引导 2020-05-03 19:57[root@localhost ~]# who -r 运行级别 5 2020-05-03 19:58

  7. ssh判断免密登陆

    ssh判断免密登陆 [root@jenkins ~]# vi /opt/release_code.sh #!/bin/bash . /etc/init.d/functions #echo $WORKS ...

  8. docker,docker-compose,harbor安装

    安装docker-ce 下载docker-ce.repo: wget -O /etc/yum.repos.d/docker-ce.repo https://download.docker.com/li ...

  9. sizeof()用法汇总-(转自风雷)

    sizeof()功能:计算数据空间的字节数 1.与strlen()比较       strlen()计算字符数组的字符数,以"\0"为结束判断,不计算为'\0'的数组元素.     ...

  10. python程序打包成exe(使用pyinstaller)

    pyinstaller下载地址:https://github.com/pyinstaller/pyinstaller/ (这个文件能够自动安装依赖项,其他版本的貌似还要自己安装依赖项) 下载之后解压到 ...