打算逐渐做一些效果来扩充原本荒凉的美术作品池。

误打误撞的成果

最终效果

星空球基础

星空球效果在各大网站上经常出现,在shadertoy上也能经常见到它的身影,实际上它的基础模型非常简单:

对于模型而言,只需要一个球。

对于光照而言,它需要一个菲涅尔边缘光来达成宇宙中星体发光的感觉,而中间的星空部分除了有星星的地方,可以全是黑色。

然后是主要的星空部分,为了营造星星的颗粒感,主要手法在于增强噪声高亮部分和低亮部分的对比度,这个部分可以用pow实现,在处理过噪声颜色之后用HDR+Bloom来营造星星的辉光效果。

星星的闪烁部分,仅靠一张噪声加上时间来做位移显然是不可取的,因为这样一来会有相当明显的连续,为了避免这种连续,我们可以将两个噪声用不同的相位相乘来形成更高的噪声频率,这样就可以产生不连续的效果,配合之前的pow处理,就可以营造比较混乱的星星闪烁效果,为了使星星更小,我们可以增大噪声的Tiling,那么一份基础的星空球shader就完成了。

shadertoy

在shadertoy上试了试水,没有写bloom。

最终效果

噪声使用的SD里输出的柏林噪声,调高了频率。

代码
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
vec3 GetSphereNormal(vec2 uv,vec3 center,float Radius){
vec2 planevec=uv-center.xy;
if(length(planevec)>Radius)return vec3(0,0,-1);
float z=sqrt(Radius*Radius-length(planevec)*length(planevec));
return normalize(vec3(planevec,z));
}
float StarNoises(vec2 uv,float speed,vec2 tiling,float power,float levels){
uv=uv*tiling;
float t=iTime*speed*0.1;
float noise1=texture(iChannel0,vec2(uv.x-t,uv.y+t)).r;
float noise2=texture(iChannel1,vec2(uv.x+t,uv.y-t)).r;
noise1=min(1.0,noise1*levels);
noise2=min(1.0,noise2*levels);
noise1=pow(noise1,power);
noise2=pow(noise2,power);
float noise=noise1*noise2;
return noise;

}
vec3 rgb2rgb(vec3 r){
return vec3(r.x/255.,r.y/255.,r.z/255.);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv=fragCoord/iResolution.xy;
uv.x/=iResolution.y/iResolution.x;


vec3 lightDir=normalize(vec3(0,0.9,1));
vec3 viewDir=normalize(vec3(0,0,1));
vec3 normal=GetSphereNormal(uv,vec3(0.5,0.5,0.5),0.45);

float halfLambert=dot(lightDir,normal)*0.5+0.5;
float noise=StarNoises(uv,0.22,vec2(5.,5.),15.0,1.35);
vec3 starColor=rgb2rgb(vec3(115.,209.,255.));
vec3 nonstarColor=vec3(0,0,0);
vec3 star=nonstarColor+(starColor-nonstarColor)*noise;
vec3 albedo=star*halfLambert;




float fre=pow(1.-dot(viewDir,normal),2.);
vec3 fresnel=rgb2rgb(vec3(115.,209.,255.))*fre;
vec3 clr=albedo+fresnel;



vec3 background=vec3(0.05,0.05,0.05);
if(normal==vec3(0,0,-1))clr=background;

​ fragColor=vec4(clr,1.);

}

(用到了插件在channel里放噪声)

我们发现在增大tiling下图片中会出现一些相同的部分,可以再用一张噪声(或者就用原来的噪声)来进行一个扰动来避免明显的重复部分,这样一个基础shader就完成了。

unity

将其移植到unity中观察效果。

最终效果

嗯,该有的都有。

但如果是这样的话,也就不需要开一篇文章专门分享这个了。

先附上shader代码:

代码
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
99
100
101
102
103
104

#ifndef EMITEINNA_NPR_Toon_StarHole_INCLUDED
#define EMITEINNA_NPR_Toon_StarHole_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "NPRLighting.hlsl"
#include "NPRInput.hlsl"

CBUFFER_START(UnityPerMaterial)
float2 _StarTiling;
float4 _DarkColor;
float4 _RimColor;
float4 _StarColorTop;
float4 _StarColorBottom;
float _Speed;
float _StarSizeMultiplier;
float _StarSizePower;
float _StarLuminance;
float _FresnelPower;
float _RimLuminance;
CBUFFER_END
TEXTURE2D(_NoiseMap); SAMPLER(sampler_NoiseMap);
TEXTURE2D(_DistortMap); SAMPLER(sampler_DistortMap);

struct Attributes
{
float4 positionOS:POSITION;
float3 normalOS:NORMAL;
float4 tangentOS:TANGENT;
float2 texcoord:TEXCOORD0;
//UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varings
{
float2 uv:TEXCOORD;
float3 positionWS:TEXCOORD1;
float3 normalWS:TEXCOORD2;
half4 tangentWS:TEXCOORD3;
float3 viewDirWS:TEXCOORD4;
float4 positionCS:SV_POSITION;
//UNITY_VERTEX_INPUT_INSTANCE_ID
//UNITY_VERTEX_OUTPUT_STEREO
};
Varings ToonStarHoleVertex(Attributes input)
{
Varings output=(Varings)0;

//UNITY_SETUP_INSTANCE_ID(input);
//UNITY_TRANSFER_INSTANCE_ID(input,output);
//UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

VertexPositionInputs vertexInput=GetVertexPositionInputs(input.positionOS.xyz);
VertexNormalInputs normalInput=GetVertexNormalInputs(input.normalOS,input.tangentOS);

output.uv=input.texcoord;
output.normalWS=normalInput.normalWS;
real sign=input.tangentOS.w*GetOddNegativeScale();
half4 tangentWS=half4(normalInput.tangentWS.xyz,sign);
output.tangentWS=tangentWS;
half3 viewDirWS=GetWorldSpaceNormalizeViewDir(vertexInput.positionWS);
output.viewDirWS=viewDirWS;

#if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR)
output.positionWS = vertexInput.positionWS;
#endif



output.positionCS = vertexInput.positionCS;

return output;
}
float StarNoises(float2 uv)
{
float2 uvTiling=uv*_StarTiling.xy;
float t=_Time.x*_Speed*0.1;
float2 mt=float2(t,-t);
float2 distort=SAMPLE_TEXTURE2D(_DistortMap,sampler_DistortMap,uv).xx;
float noise1=SAMPLE_TEXTURE2D(_NoiseMap,sampler_NoiseMap,uvTiling+mt-distort).r;
float noise2=SAMPLE_TEXTURE2D(_NoiseMap,sampler_NoiseMap,uvTiling-mt+distort).r;
noise1=min(1.0,noise1*_StarSizeMultiplier);
noise2=min(1.0,noise2*_StarSizeMultiplier);
noise1=pow(noise1,_StarSizePower);
noise2=pow(noise2,_StarSizePower);
float noise=noise1*noise2;
return noise;
}
float4 ToonStarHoleFragment(Varings input):SV_TARGET0
{
float3 L=normalize(GetMainLight().direction);
float3 V=normalize(input.viewDirWS);
float3 N=normalize(input.normalWS);
float halfLambert=max(0,dot(L,N))*0.5+0.5;
float starNoise=StarNoises(input.uv)*halfLambert;
float3 starColor=lerp(_StarColorBottom.rgb,_StarColorTop.rgb,input.uv.y);
float3 albedo=lerp(_DarkColor, starColor*_StarLuminance,starNoise);
albedo=albedo;

float fresnel=pow(1-dot(V,N),_FresnelPower);
float3 fresnelColor=_RimColor*fresnel*_RimLuminance;

return float4(albedo+fresnelColor,1);
}
#endif

仍然是低频采样高频问题

仔细操作一下这个球,我们会发现离这个球越近,这个球就越亮,或者说这个球上的高亮点明显地增多了。

图片

拉近后

图片

分析一下原因,首先,在这个shader里采样噪声使用的是球上的UV坐标,和摄像机位置、光源位置等完全没有关系,也就是说这个uv坐标在不同距离下计算出来的结果导致了这个bug特性

其实很容易想出来为什么:因为通过刚刚的操作,我们创造出了一个相当高频的噪声,而球形中uv坐标的采样级精度有限,有些高亮度的点并不能采样到,而在离球形更近的时候,因为片元数量增加,采样精度也因此增加,所以采样出的高亮点自然更多了。

具体可以看这张示意图:

图片

很显然,在采样率从500升级到1000之后,采样出的高亮点直接多了两个,而高亮点的增加造成的结果相当明显,这也就导致在高亮点增多到一定个数的时候整个球都会变成亮的。

解决方法

降低噪声频率

最简单的解决方法毫无疑问是:不要使用这么高频的噪声,但是回想一下我为什么要使用这么高频的噪声?因为想要更加随机的效果,同时我想要星星的大小更小,如果降低噪声的频率那么肯定会让星星的大小变大,从而导致整个效果非常粗糙,于是就不得不去通过multiply和pow去调整整个显示效果,让原来的效果显得非常复杂。这种方法肯定是最划算的,至于是否要使用就要看具体的需求了。

超采样

既然已经知道了采样频率能够决定最终的效果,那么为什么不去手动规定一个采样频率呢?依然拿上面的渣手绘图举例。我可以在300/500的位置手动去取值300.5/500、299.5/500,然后做一个加权平均,这样就相当于进行到了一个更高频率的采样,虽然肯定不如在高清时候显示的那么可靠,但是不会有那么明显的差别。

具体而言,这个做法的写法是规定一个目标分辨率,然后对于每个采样点,根据分辨率去采样周围的一系列点求加权平均。这个做法最大的问题显而易见——时间复杂度直接成倍增加,并且会导致整体亮度增大,不好调参。

但是如果是已经知道当前分辨率的话就不一定了,我可以根据当前分辨率来计算出需要采样多少个点,这样一来使得整个算法复杂度只和目标分辨率有关。而我的目标并不是完全精确的采样,只需要保证在不同分辨率下采样的结果近似相同就可以了,这在屏幕后处理星空效果的时候完全可以用上(虽然好像这种效果挺费的)。

摆烂

越近星空越亮何尝不是一种特性?把这个星空球稍微改装就形成了一个炫酷的进场效果,它还得谢谢咱呢(见文章开头)。