前言:偶然间在Sandbox里发现一个很炫酷的效果,所以尝试在unity里实现了一下!另外非常推荐Sandbox和ShaderToy这两个网站,对于图形学算法的学习研究很有帮助,可以实时编译实时看到效果。由于这两个网站上面的效果都是以屏幕UV来绘制的,所以最初在Unity里的后处理阶段进行模拟比较好,以下最终效果!
核心思路:这个算法的思路非常巧妙也很简单,就是画圆,一个在屏幕中心画表示太阳的位置,一个在鼠标位置画,用来模拟月球!然后判断两者的距离,小于半径则被遮挡,绘制成黑色!
具体实现过程分析:
- 圆的绘制:我们知道在一个平面内,一动点以一定点为中心,以一定长度为距离旋转一周所形成的封闭曲线叫做圆。正好,我们可以用一个不限定方向只限定长度的向量来绘制。这样不考虑Z轴的情况下,在XY平面内由无数个相同长度的向量形成图形就是圆。向量的长度也就是圆的半径,如图所示:
所以,只要在UV空间下算出到UV中心点相同距离的UV对应的像素,然后填充相应的颜色即可,例如以OpenGL坐标系为例,在屏幕中心绘制一个半径为0.09的圆形,如图所示:
代码如下
half3 day = half3(0.2, 0.3, 0.5);
if (length(pos) < 0.09)
{
return half3(1.2, 1.1, 0.9);
}
return day;//day就是除了圆以外的颜色值即天空的颜色
实现效果如下
2.然后绘制鼠标处的圆,我们可以从C#中获取鼠标在屏幕中的位置,转换到UV空间(0~1)范围内,因为我们都是以(0.5,0.5)为中心的坐标系来进行计算,为了对应(0,0)为中心坐标系的各点,往(-0.5,-0.5)方向做一次平移
最后根据屏幕比例做适配,代码如下:
half2 gl_FragCoord = ((i.srcPos.xy / i.srcPos.w) );
half2 m = mouse.xy/ _ScreenParams.xy - 0.5;//(mouse是传进来鼠标在屏幕中的参数,_ScreenParams是unity内置取屏幕中的坐标的参数,两者都以pixel为单位)
float aspect = _ScreenParams.x / _ScreenParams.y;
half2 position = (gl_FragCoord.xy ) - 0.5;
position.x *= aspect;
m.x *= aspect;
绘制鼠标处的圆,算出像素和鼠标之间的距离在半径范围内则绘制成黑色
if (distance(pos, m) < 0.08)
{
return half3(0,0.0,0);
}
实现效果如下
3.绘制出白天和黑天的过渡
算出鼠标到中心的距离小于中心圆的半径,则判定太阳被遮挡,背景颜色往黑色转换,反之往白天颜色转换,但是这样的转换太过生硬,没有平缓过渡的过程,如图:
float lm = length(m);
if (lm < 0.09) {
return lerp(night, day, lm );
}
所以我们把判定的半径范围加大,这样在靠近太阳的时候,天空就有颜色的变化,然后根据鼠标距离算插入值的时候将精度缩小一倍,精度缩小的值应该是经验值得出的,这样可以使月亮完全进入太阳范围内时,天空再完全变暗,修改后效果如下:
float lm = length(m);
if (lm < 0.5) {
return lerp(night, day, lm / 0.5);
}
4.绘制出光晕效果
这个日食的精髓就在这里,想法很巧妙,简单的计算就能模拟出遮光的效果,核心原理和上面一样就是判断一个圆是否在另外一个圆内,只不过把之前的基础圆扩展成n个,从物理的角度来考虑,太阳肯定不仅仅是只有半径范围内发亮,肯定是中心最亮,随着距离亮度不断在衰减。所以我们按照中心距离绘制出不同半径的圆,然后进行叠加,但是直接叠加,肯定屏幕会爆掉,我们要根据叠加的次数对亮度进行衰减,代码如下:
half3 light = half3(1.0,1.0,1.0);
half iterations = 10;
half2 incr = position / float(iterations);
half2 p = half2(0.0, 0.0) + incr;
for (int i = 2; i < iterations; i++)
{
light += getColor(p, m);
p += incr;
}
light /= float(iterations);//亮度衰减
注意:大家应该很奇怪边缘黄色的效果是怎么出来的,大家应该发现前面绘制太阳的时候,为什么不直接给half3(1,1,1),而是一个溢出值
if (length(pos) < 0.09)
{
return half3(1.2, 1.1, 0.9);
}
我们把溢出值单独提出来,转换成颜色值在ps查看对应的颜色
发现这是一个暗黄色的值
经过不断叠加颜色会越来越偏黄色
n个太阳圆和n个月亮圆之间判断出遮挡关系
效果如下:
5.绘制星空
利用正弦函数构造一个随机分布的函数绘制出星空
完整代码如下:
Shader "Unlit/RiShi"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
const int iterations = 50;
uniform float time;
uniform half3 mouse;
// uniform half2 resolution;
half3 getColor(half2 pos, half2 m) {
half3 day = half3(0.2, 0.3, 0.5);
half3 night = half3(0.1, 0.1, 0.1);
if (distance(pos, m) < 0.08) {
return half3(0,0.0,0);
}
if (length(pos) < 0.09) {
return half3(1.2, 1.1, 0.9);
}
float lm = length(m);
if (lm < 0.5) {
return lerp(night, day, lm / 0.5);
}
return day;
}
float rand(float x) {
float res = 0.0;
for (int i = 0; i < 5; i++) {
res += 0.240 * float(i) * sin(x * 0.68171 * float(i));
}
return res;
}
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
float4 srcPos : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.srcPos = ComputeScreenPos(o.vertex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half2 gl_FragCoord = ((i.srcPos.xy / i.srcPos.w) * _ScreenParams.xy);
half2 m = mouse.xy/ _ScreenParams.xy - 0.5;
float aspect = _ScreenParams.x / _ScreenParams.y;
half2 position = (gl_FragCoord.xy / _ScreenParams.xy) - 0.5;
position.x *= aspect;
m.x *= aspect;
half3 color = getColor(position, m);
half3 light = half3(1.0,1.0,1.0);
half iterations = 50;
half2 incr = position / float(iterations);
half2 p = half2(0.0, 0.0) + incr;
for (int i = 2; i < iterations; i++) {
light += getColor(p, m);
p += incr;
}
light /= float(iterations) * max(.01, dot(position, position)) * 40.0;
half2 star = gl_FragCoord.xy;
if (rand(star.y * star.x) >= 2.1 && rand(star.y + star.x) >= .7) {
float lm = length(m);
if (lm < 0.15) {
color = lerp(half3(2.0,2,2), half3(0.2, 0.3, 0.5), lm / 0.15);
}
}
if (distance(position, m) < 0.05) {
color = half3(0,0,0);
}
//return half4(color, 1);
return half4(color+ light, 1.0);
}
ENDCG
}
}
}
后续:这种方法过于暴力,本来打算写一版公式解析的方法,但是没想到知乎的大佬分分钟就给写了出来,我就直接把链接给贴过来了
杨超:日食效果优化--解析法godrayzhuanlan.zhihu.com