正是一个炎夏,又到了整活的好时候。最近抽些时间研究下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. Java中对象池的本质是什么?(实战分析版)

    简介 对象池顾名思义就是存放对象的池,与我们常听到的线程池.数据库连接池.http连接池等一样,都是典型的池化设计思想. 对象池的优点就是可以集中管理池中对象,减少频繁创建和销毁长期使用的对象,从而提 ...

  2. Excel导出数据Excel.Application组件权限设置方法

    很多网络应用系统都会涉及到数据采用Excel方式导出的模块,部分朋友问我到底怎么弄,其实方式很多种,目前比较优秀的方式还是直接用Excel的Excel.Application方式比较合适. 采用Exc ...

  3. CSS变量和浏览器前缀

    一.CSS变量 CSS变量是CSS的新特性,大多数浏览器都实现了这个功能,使用CSS变量有利代码复用,而且当我们修改变量值时,所有引用该变量的属性都会发生改变. 定义变量后可以有两种使用方法,第一种时 ...

  4. [转载]屏幕PPI、分辨率到底需要多大才能满足?

    屏幕PPI.分辨率到底需要多大才能满足? 郝蛋儿 江湖骗子 13 人赞同了该文章 最近想买一个43寸的电视,720P和1080P差了500大洋.我不禁纠结了起来.看网上争得面红耳赤,有的人说不如108 ...

  5. Yarn 集群环境 HA 搭建

    环境准备 确保主机搭建 HDFS HA 运行环境 步骤一:修改 mapred-site.xml 配置文件 [root@node-01 ~]# cd /root/apps/hadoop-3.2.1/et ...

  6. linux进阶之计划任务及压缩归档

    本节内容 1. at一次性计划任务(atd) at 时间点 command ctrl+d:保存 -l:查看计划任务 atrm:删除计划任务 atq:查看计划任务 2. crontab周期性计划任务(c ...

  7. S5 Linux信息显示与搜索文件命令

    5.1-5 uname.hostname.dmesg.stat.du 5.6 date:显示与设置系统时间 5.7 echo:显示一行文本 5.8-12 watch.which.whereis.loc ...

  8. redis的rehash过程

    在扩容和收缩的时候,如果哈希字典中有很多元素,一次性将这些键全部rehash到ht[1]的话,可能会导致服务器在一段时间内停止服务.所以,采用渐进式rehash的方式,详细步骤如下: 为ht[1]分配 ...

  9. 云原生ASP.NET Core程序的可监测性和可观察性

    分布式应用程序很复杂,给开发人员调试和修复生产问题带来了一系列挑战.尽管微服务架构可帮助维持一支规模较小,可以自主工作并专注于独立业务团队,但由于其分布式性质,它带来了新的挑战.例如,在业务交易过程中 ...

  10. SystemVerilog MCDF比较器

    checker肩负了模拟设计行为和功能检查任务. 功能: 缓存从各个monitor手机到的数据. ton过比较器检查实际收集到的DUT输出端口数据是否同reference module(参考模型)产生 ...