打算逐渐做一些效果来扩充原本荒凉的美术作品池。
误打误撞的成果
星空球基础
星空球效果在各大网站上经常出现,在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,然后做一个加权平均,这样就相当于进行到了一个更高频率的采样,虽然肯定不如在高清时候显示的那么可靠,但是不会有那么明显的差别。
具体而言,这个做法的写法是规定一个目标分辨率,然后对于每个采样点,根据分辨率去采样周围的一系列点求加权平均。这个做法最大的问题显而易见——时间复杂度直接成倍增加,并且会导致整体亮度增大,不好调参。
但是如果是已经知道当前分辨率的话就不一定了,我可以根据当前分辨率来计算出需要采样多少个点,这样一来使得整个算法复杂度只和目标分辨率有关。而我的目标并不是完全精确的采样,只需要保证在不同分辨率下采样的结果近似相同就可以了,这在屏幕后处理星空效果的时候完全可以用上(虽然好像这种效果挺费的)。
摆烂
越近星空越亮何尝不是一种特性?把这个星空球稍微改装就形成了一个炫酷的进场效果,它还得谢谢咱呢(见文章开头)。