Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
PU 光線追蹤是當(dāng)今的熱門話題,所以讓我們來(lái)談?wù)勊〗裉煳覀儗⒐饩€追蹤一個(gè)單個(gè)球體。
使用片段著色器。
是的,我知道。并不特別花哨。你可以在 Shadertoy 上搜索并獲得數(shù)百個(gè)示例(https://www.shadertoy.com/results?query=sphere)。甚至已經(jīng)有一些很棒的教程教你如何做 球體Imposter
(https://paroj.github.io/gltut/Illumination/Tutorial 13.html),
這就是我們要做的。那么我為什么要寫另一篇關(guān)于它的文章呢?它甚至不是正確類型的 GPU 光線追蹤!
好吧,因?yàn)楣饩€追蹤部分并不是我真正要關(guān)注的部分。這篇文章更多的是關(guān)于如何在 Unity 中將不透明的光線追蹤或光線行進(jìn)物體注入到光柵化場(chǎng)景中。但也介紹了一些處理渲染球體Imposter的額外技巧,這些技巧并不總是顯而易見(jiàn)或被我見(jiàn)過(guò)的其他教程所涵蓋。在這篇文章的最后,我們將得到一個(gè)緊湊的四邊形上的球體Imposter,它支持多個(gè)燈光、陰影投射、陰影接收和正交相機(jī),用于內(nèi)置的前向渲染器,幾乎完美地模擬了一個(gè)高多邊形網(wǎng)格。無(wú)需額外的 C# 腳本。
如引言中所述,這是一個(gè)已經(jīng)被廣泛探索的領(lǐng)域。繪制球體的準(zhǔn)確高效的數(shù)學(xué)方法已經(jīng)為人所知。所以我只是要從 Inigo Quilez 的代碼中竊取適用的函數(shù),來(lái)創(chuàng)建一個(gè)基本的光線追蹤球體著色器,我們可以將其貼到立方體網(wǎng)格上。
https://www.iquilezles.org/www/articles/intersectors/intersectors.htm
Inigo 的示例都是用 GLSL 編寫的。所以我們需要稍微修改一下代碼才能讓它適用于 HLSL。幸運(yùn)的是,對(duì)于這個(gè)函數(shù)來(lái)說(shuō),這實(shí)際上只需要將 vec 替換成 float。
float sphIntersect( float3 ro, float3 rd, float4 sph )
{
float3 oc=ro - sph.xyz;
float b=dot( oc, rd );
float c=dot( oc, oc ) - sph.w*sph.w;
float h=b*b - c;
if( h<0.0 ) return -1.0;
h=sqrt( h );
return -b - h;
}
該函數(shù)接受 3 個(gè)參數(shù):ro(光線起點(diǎn))、rd(歸一化的光線方向)和 sph(球體位置 xyz 和半徑 w)。它返回光線從起點(diǎn)到球體表面的長(zhǎng)度,或者在未命中時(shí)返回 -1.0。簡(jiǎn)單明了。所以我們只需要這三個(gè)向量,我們就可以得到一個(gè)漂亮的球體。
光線起點(diǎn)可能是最容易獲得的點(diǎn)。對(duì)于 Unity 著色器來(lái)說(shuō),它將是相機(jī)位置。方便地傳遞給全局著色器 _WorldSpaceCameraPos 中的每個(gè)著色器。對(duì)于正交相機(jī)來(lái)說(shuō),它稍微復(fù)雜一些,但幸運(yùn)的是,我們不必?fù)?dān)心。
不祥的預(yù)兆
對(duì)于球體位置,我們可以使用我們正在應(yīng)用著色器的物體的世界空間位置。這可以通過(guò) unity_ObjectToWorld._m03_m13_m23 從物體的變換矩陣中輕松提取。我們可以將半徑設(shè)置為某個(gè)任意值。為了沒(méi)有特別的理由,讓我們選擇 0.5。
最后是光線方向。這只是從相機(jī)到我們代理網(wǎng)格的世界位置的方向。通過(guò)在頂點(diǎn)著色器中計(jì)算它并將向量傳遞給片段著色器,我們可以很容易地獲得它。
float3 worldPos=mul(unity_ObjectToWorld, v.vertex);
float3 rayDir=_WorldSpaceCameraPos.xyz - worldPos;
請(qǐng)注意,在頂點(diǎn)著色器中對(duì)其進(jìn)行歸一化非常重要。你需要在片段著色器中執(zhí)行此操作,否則插值的值將無(wú)法正常工作。我們正在插值的值是表面位置,而不是實(shí)際的光線方向。
但是經(jīng)過(guò)所有這些,我們得到了光線追蹤球體所需的三個(gè)值。
現(xiàn)在我說(shuō)上面的函數(shù)返回光線長(zhǎng)度。所以要獲得球體表面的實(shí)際世界空間位置,你將歸一化的光線乘以光線長(zhǎng)度,然后加上光線起點(diǎn)。你甚至可以通過(guò)從球體位置減去表面位置并進(jìn)行歸一化來(lái)獲得世界法線。我們將光線長(zhǎng)度傳遞給 clip() 函數(shù),以隱藏球體外部的任何東西,因?yàn)樵摵瘮?shù)在未命中時(shí)返回 -1.0。
球體Imposter的最后一個(gè)要點(diǎn)是 z 深度。如果我們希望我們的球體與世界正確地相交,我們需要從片段著色器中輸出球體的深度。否則,我們將被迫使用我們用來(lái)渲染的網(wǎng)格的深度。這實(shí)際上比聽(tīng)起來(lái)容易得多。由于我們已經(jīng)在片段著色器中計(jì)算了世界位置,我們可以應(yīng)用我們?cè)陧旤c(diǎn)著色器中使用的相同視圖和投影矩陣來(lái)獲得 z 深度。Unity 甚至包含一個(gè)方便的 UnityWorldToClipPos() 函數(shù),使它變得更加容易。然后,它需要一個(gè)使用 SV_Depth 的輸出參數(shù),其中包含剪切空間位置的 z 除以其 w。
將所有這些與一些基本的光照結(jié)合起來(lái),你就會(huì)得到類似這樣的東西:
它看起來(lái)像一個(gè)球體,但實(shí)際上是一個(gè)立方體。
讓所有男人都為之驚嘆的一個(gè)非常圓的立方體
Shader "Basic Sphere Impostor"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="AlphaTest" "DisableBatching"="True" }
LOD 100
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 rayDir : TEXCOORD0;
float3 rayOrigin : TEXCOORD1;
};
v2f vert (appdata v)
{
v2f o;
// get world position of vertex
// using float4(v.vertex.xyz, 1.0) instead of v.vertex to match Unity's code
float3 worldPos=mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0));
// calculate and world space ray direction and origin for interpolation
o.rayDir=worldPos - _WorldSpaceCameraPos.xyz;
o.rayOrigin=_WorldSpaceCameraPos.xyz;
o.pos=UnityWorldToClipPos(worldPos);
return o;
}
// https://www.iquilezles.org/www/articles/spherefunctions/spherefunctions.htm
float sphIntersect( float3 ro, float3 rd, float4 sph )
{
float3 oc=ro - sph.xyz;
float b=dot( oc, rd );
float c=dot( oc, oc ) - sph.w*sph.w;
float h=b*b - c;
if( h<0.0 ) return -1.0;
h=sqrt( h );
return -b - h;
}
half3 _LightColor0;
half4 frag (v2f i, out float outDepth : SV_Depth) : SV_Target
{
// ray origin
float3 rayOrigin=i.rayOrigin;
// normalize ray vector
float3 rayDir=normalize(i.rayDir);
// sphere position
float3 spherePos=unity_ObjectToWorld._m03_m13_m23;
// ray box intersection
float rayHit=sphIntersect(rayOrigin, rayDir, float4(spherePos, 0.5));
// above function returns -1 if there's no intersection
clip(rayHit);
// calculate world space position from ray, front hit ray length, and ray origin
float3 worldPos=rayDir * rayHit + rayOrigin;
// world space surface normal
float3 worldNormal=normalize(worldPos - spherePos);
// basic lighting
half3 worldLightDir=_WorldSpaceLightPos0.xyz;
half ndotl=saturate(dot(worldNormal, worldLightDir));
half3 lighting=_LightColor0 * ndotl;
// ambient lighting
half3 ambient=ShadeSH9(float4(worldNormal, 1));
lighting +=ambient;
// output modified depth
float4 clipPos=UnityWorldToClipPos(worldPos);
outDepth=clipPos.z / clipPos.w;
return half4(lighting, 1.0);
}
ENDCG
}
}
}
好吧,這并不太令人興奮。我們應(yīng)該在上面放一個(gè)紋理。為此,我們需要 UV,幸運(yùn)的是,對(duì)于球體來(lái)說(shuō),這些 UV 非常容易獲得。
讓我們?cè)谏厦尜N一個(gè)等距矩形紋理。為此,我們只需要將法線方向輸入到 atan2() 和 acos() 中,我們就會(huì)得到類似這樣的東西:
float2 uv=float2(
// atan 返回 -pi 到 pi 之間的值
// 所以我們除以 pi * 2 來(lái)得到 -0.5 到 0.5
atan2(normal.z, normal.x) / (UNITY_PI * 2.0),
// acos 在頂部返回 0.0,在底部返回 pi
// 所以我們將 y 翻轉(zhuǎn)以與 Unity 的 OpenGL 風(fēng)格對(duì)齊
// 紋理 UV,所以 0.0 在底部
acos(-normal.y) / UNITY_PI
);fixed4 col=tex2D(_MainTex, uv);
地球,最后的疆域。
看看,我們得到一個(gè)完美的……等等。這是什么!?
那是格林威治子午線嗎?
這是一個(gè) UV 縫!我們?cè)趺磿?huì)出現(xiàn) UV 縫呢?好吧,這取決于 GPU 如何為 mip 貼圖計(jì)算 mip 層級(jí)。
GPU 通過(guò)所謂的屏幕空間偏導(dǎo)數(shù)來(lái)計(jì)算 mip 層級(jí)。粗略地說(shuō),這是值從一個(gè)像素到它旁邊的一個(gè)像素(向上或向下)的變化量。GPU 可以為每組 2x2 像素計(jì)算此值,因此 mip 層級(jí)由這些 2x2“像素四邊形”中 UV 的變化量決定。當(dāng)我們?cè)谶@里計(jì)算 UV 時(shí),atan2() 突然在兩個(gè)像素之間從大約 0.5 跳到大約 -0.5。這使得 GPU 認(rèn)為整個(gè)紋理在這兩個(gè)像素之間顯示。因此,它會(huì)使用它擁有的絕對(duì)最小的 mip 貼圖來(lái)響應(yīng)。
那么我們?nèi)绾谓鉀Q這個(gè)問(wèn)題呢?當(dāng)然,通過(guò)禁用 mip 貼圖!
不不不! 我們絕對(duì)不會(huì)這樣做。 但這是你通常會(huì)找到的解決大多數(shù) mip 貼圖相關(guān)問(wèn)題的方案。相反,Marco Tarini 提供了一個(gè)很好的解決方案。
http://vcg.isti.cnr.it/~tarini/no-seams/
這個(gè)想法是使用兩個(gè) UV 集,它們?cè)诓煌奈恢糜锌p合。對(duì)于我們的特定情況,由 atan2() 計(jì)算的經(jīng)度 UV 已經(jīng)是 -0.5 到 0.5 的范圍,所以我們只需要一個(gè) frac() 來(lái)將它們轉(zhuǎn)換為 0.0 到 1.0 的范圍。然后使用相同的偏導(dǎo)數(shù)來(lái)選擇變化最小的 UV 集。神奇的函數(shù) fwidth() 給出了值在任何屏幕空間方向上的變化量。
// -0.5 到 0.5 的范圍
float phi=atan2(worldNormal.z, worldNormal.x) / (UNITY_PI * 2.0);
// 0.0 到 1.0 的范圍
float phi_frac=frac(phi);float2 uv=float2(
// 使用一個(gè)小偏差來(lái)優(yōu)先考慮第一個(gè)“UV 集”
fwidth(phi) < fwidth(phi_frac) - 0.001 ? phi : phi_frac,
acos(-worldNormal.y) / UNITY_PI
);
現(xiàn)在我們沒(méi)有縫合了!
我保證它沒(méi)有隱藏在另一邊
** 后記:我注意到這種技術(shù)可能只在使用 Direct3D、集成英特爾 GPU 或(某些?)Android OpenGLES 設(shè)備時(shí)才能正常工作。在桌面設(shè)備上使用 OpenGL 時(shí),* fwidth() 函數(shù)可能使用比 GPU 用于確定 mip 層級(jí)的精度更高的導(dǎo)數(shù),這意味著縫合仍然可見(jiàn)。Metal 保證始終以更高的精度運(yùn)行。Vulkan 可以通過(guò)使用粗導(dǎo)數(shù)函數(shù)來(lái)強(qiáng)制以較低的精度運(yùn)行,但截至撰寫本文時(shí),Unity 似乎沒(méi)有正確地轉(zhuǎn)譯粗導(dǎo)數(shù)或精導(dǎo)數(shù)。我寫了一篇后續(xù)文章,其中介紹了一些替代解決方案:
https://bgolus.medium.com/distinctive-derivative-differences-cce38d36797b
或者,你可以直接使用立方體貼圖。Unity 可以為你將導(dǎo)入的等距矩形紋理轉(zhuǎn)換為立方體貼圖。但這意味著你將失去各向異性過(guò)濾。立方體貼圖紋理采樣的 UVW 本質(zhì)上只是球體的法線。不過(guò),你確實(shí)需要翻轉(zhuǎn) x 軸或 z 軸,因?yàn)榱⒎襟w貼圖被假定為從球體的“內(nèi)部”進(jìn)行觀察,而在這里我們希望它映射到外部。
此時(shí),如果我們將現(xiàn)有的光線追蹤球體著色器與使用相同等距矩形 UV 的實(shí)際高多邊形網(wǎng)格球體進(jìn)行比較,你可能會(huì)注意到一些奇怪的事情。看起來(lái)光線追蹤球體周圍有一個(gè)輪廓,而網(wǎng)格沒(méi)有。一個(gè)非常鋸齒的輪廓。
Imposter的粗糙“輪廓”。
原因是我們討厭的導(dǎo)數(shù)再次出現(xiàn)了。我們錯(cuò)過(guò)了另一個(gè) UV 縫!在網(wǎng)格上,導(dǎo)數(shù)是針對(duì)每個(gè)像素四邊形、每個(gè)三角形計(jì)算的。事實(shí)上,如果一個(gè)三角形只接觸到一個(gè) 2x2 像素四邊形中的一個(gè)像素,GPU 仍然會(huì)為所有 4 個(gè)像素運(yùn)行片段著色器!這樣做的好處是,它可以準(zhǔn)確地計(jì)算出合理的導(dǎo)數(shù),從而防止在真實(shí)網(wǎng)格上出現(xiàn)此問(wèn)題。但我們?cè)谇蝮w外部沒(méi)有一個(gè)好的 UV,該函數(shù)在未命中時(shí)只返回一個(gè)常數(shù) -1.0,因此我們?cè)谇蝮w外部有錯(cuò)誤的 UV。如果在著色器中注釋掉 clip() 和 outDepth 行,我們可以清楚地看到這一點(diǎn)。
隱藏的 UV 縫
我們想要的是讓 UV 接近球體可見(jiàn)邊緣的值,或者可能剛剛超過(guò)邊緣。這令人驚訝地難以計(jì)算。但我們可以通過(guò)找到光線到球體中心的最近點(diǎn)來(lái)獲得一個(gè)相當(dāng)接近的值。在球體邊緣,這是 100% 準(zhǔn)確的,但當(dāng)離球體越來(lái)越遠(yuǎn)時(shí),它會(huì)開(kāi)始向相機(jī)方向彎曲。但這很便宜,足以消除這個(gè)問(wèn)題,并且與完全正確的修復(fù)幾乎沒(méi)有區(qū)別。
更棒的是,當(dāng)球體相交函數(shù)返回 -1.0 時(shí),我們可以通過(guò)用一個(gè) dot() 替換光線長(zhǎng)度來(lái)應(yīng)用此修復(fù)。兩個(gè)向量的點(diǎn)積的一個(gè)超級(jí)能力是,如果至少一個(gè)向量是歸一化的,則輸出是另一個(gè)向量沿歸一化向量方向的幅度。這對(duì)于獲取某個(gè)方向上的距離非常有用,例如相機(jī)沿視圖光線距離球體樞軸的距離。
// 相同的球體相交函數(shù)
float rayHit=sphIntersect(rayOrigin, rayDir, float4(0,0,0,0.5));
// 如果是 -1.0,則剪切以在未命中時(shí)隱藏球體
clip(rayHit);
// 點(diǎn)積獲取最靠近球體的點(diǎn)處的光線長(zhǎng)度
rayHit=rayHit < 0.0 ? dot(rayDir, spherePos - rayOrigin) : rayHit;
不再有縫合。
所以一切都進(jìn)展順利,但如果我們想做一個(gè)更大的球體或旋轉(zhuǎn)它怎么辦?我們可以移動(dòng)網(wǎng)格位置,球體會(huì)隨之移動(dòng),但其他所有東西都被忽略了。
我們可以手動(dòng)更改球體半徑,但隨后你必須手動(dòng)保持你正在使用的網(wǎng)格同步。所以,從物體變換本身提取縮放比例會(huì)更容易。我們可以應(yīng)用一個(gè)任意的旋轉(zhuǎn)矩陣,但同樣,如果我們能直接使用物體變換,那就更容易了。
或者,我們可以做一些更簡(jiǎn)單的事情,在物體空間中進(jìn)行光線追蹤!這帶來(lái)了一些其他的好處,我們將在后面介紹。但在那之前,我們想要在著色器代碼中添加幾行。首先,我們想要使用 unity_WorldToObject 矩陣將光線起點(diǎn)和光線方向在頂點(diǎn)著色器中轉(zhuǎn)換為物體空間。在片段著色器中,我們不再需要從變換中獲取世界空間物體位置,因?yàn)榍蝮w現(xiàn)在可以位于物體的原點(diǎn)。
// 頂點(diǎn)著色器
float3 worldSpaceRayDir=worldPos - _WorldSpaceCameraPos.xyz;
// 只想旋轉(zhuǎn)和縮放 dir 向量,所以 w=0
o.rayDir=mul(unity_WorldToObject, float4(worldSpaceRayDir, 0.0));
// 需要對(duì)起點(diǎn)向量應(yīng)用完整的變換
o.rayOrigin=mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1.0));// 片段著色器
float3 spherePos=float3(0,0,0);
僅通過(guò)添加上面的代碼到我們的著色器,你就可以旋轉(zhuǎn)和縮放游戲物體,球體也會(huì)按預(yù)期進(jìn)行縮放和旋轉(zhuǎn)。它甚至支持非均勻縮放!請(qǐng)記住,著色器中的所有這些“世界空間”位置現(xiàn)在都在物體空間中。所以我們需要將法線和球體表面位置轉(zhuǎn)換為世界空間。只需確保使用物體空間法線作為 UV。
// 現(xiàn)在獲取物體空間表面位置,而不是世界空間
float3 objectSpacePos=rayDir * rayHit + rayOrigin;// 仍然需要在物體空間中對(duì)其進(jìn)行歸一化以用于 UV
float3 objectSpaceNormal=normalize(objectSpacePos);float3 worldNormal=UnityObjectToWorldNormal(objectSpaceNormal);
float3 worldPos=mul(unity_ObjectToWorld, float4(objectSpacePos, 1.0));
大、小和可怕的三明治地球。
其他優(yōu)勢(shì)包括更好的整體精度,因?yàn)閷?duì)所有內(nèi)容使用世界空間會(huì)在遠(yuǎn)離原點(diǎn)時(shí)導(dǎo)致一些精度問(wèn)題。在使用物體空間時(shí),這些問(wèn)題至少可以部分避免。這也意味著我們可以刪除幾個(gè)地方對(duì) spherePos 的使用,因?yàn)樗际橇悖瑥亩?jiǎn)化代碼。
到目前為止,我們一直在使用立方體網(wǎng)格。在某些情況下,使用立方體確實(shí)有一些好處,但我承諾在本文的標(biāo)題中使用四邊形。而且,實(shí)際上沒(méi)有充分的理由為一個(gè)球體使用整個(gè)立方體。在側(cè)面有很多浪費(fèi)的空間,我們?cè)谀抢镏Ц读虽秩厩蝮w的成本,而我們知道它不會(huì)在那里。尤其是默認(rèn)的 Unity 立方體,它有 24 個(gè)頂點(diǎn)!為什么還要浪費(fèi)計(jì)算額外的 20 個(gè)頂點(diǎn)?
有很多公告牌著色器的示例。它們的基本原理是忽略物體的變換的旋轉(zhuǎn)(和縮放!),而是將網(wǎng)格對(duì)齊到某個(gè)方向以面向相機(jī)。
這可能是最常見(jiàn)的版本。這是通過(guò)將樞軸位置轉(zhuǎn)換為視圖空間,并將頂點(diǎn)偏移量添加到視圖空間位置來(lái)實(shí)現(xiàn)的。這樣做相對(duì)便宜。請(qǐng)記住更新光線方向以匹配。
// 從變換矩陣中獲取物體的世界空間樞軸
float3 worldSpacePivot=unity_ObjectToWorld._m03_m13_m23;// 轉(zhuǎn)換為視圖空間
float3 viewSpacePivot=mul(UNITY_MATRIX_V, float4(worldSpacePivot, 1.0));// 物體空間頂點(diǎn)位置 + 視圖樞軸=公告牌四邊形
float3 viewSpacePos=v.vertex.xyz + viewSpacePivot;// 從視圖空間位置計(jì)算物體空間光線 dir
o.rayDir=mul(unity_WorldToObject,
mul(UNITY_MATRIX_I_V, float4(viewSpacePos, 0.0))
);// 應(yīng)用投影矩陣以獲取剪切空間位置
o.pos=mul(UNITY_MATRIX_P, float4(viewSpacePos, 1.0));
但是,如果我們只是將上面的代碼添加到我們的著色器中,球體就會(huì)出現(xiàn)一些問(wèn)題。它在邊緣被剪切,尤其是在球體位于側(cè)面或靠近相機(jī)時(shí)。
想得太超出了范圍。
這是因?yàn)樗倪呅问且粋€(gè)平面,而球體不是。球體有一定的深度。由于透視,球體的體積將覆蓋比四邊形更多的屏幕!
藝術(shù)家對(duì)犯罪現(xiàn)場(chǎng)的再現(xiàn)
你可能會(huì)使用的解決方案是將公告牌按某個(gè)任意量進(jìn)行縮放。但這并不能完全解決問(wèn)題,因?yàn)槟惚仨殞⑺倪呅畏糯蠛芏唷S绕涫窃谀憧拷蝮w或具有非常寬的視場(chǎng)時(shí)。這在一定程度上違背了使用四邊形而不是立方體的初衷。事實(shí)上,與立方體相比,即使是相對(duì)較小的縮放比例增加,現(xiàn)在也有更多像素渲染了空的空間。
幸運(yùn)的是,我們可以做得更好。一個(gè)部分的解決方案是使用面向相機(jī)的公告牌,而不是面向視圖的公告牌,并將四邊形稍微拉向相機(jī)。面向視圖的公告牌和面向相機(jī)的公告牌之間的區(qū)別在于,面向視圖的公告牌與視圖所面向的方向?qū)R。面向相機(jī)的公告牌面向相機(jī)的位置。區(qū)別可能很細(xì)微,代碼也稍微復(fù)雜一些。
我們不再在視圖空間中執(zhí)行操作,而是需要構(gòu)建一個(gè)旋轉(zhuǎn)矩陣,將四邊形旋轉(zhuǎn)到面向相機(jī)。這聽(tīng)起來(lái)比實(shí)際操作更可怕。你只需要獲取從物體位置指向相機(jī)的向量、前進(jìn)向量,并使用叉積來(lái)獲取向上向量和向右向量。將這三個(gè)向量放在一起,你就得到了一個(gè)旋轉(zhuǎn)矩陣。
float3 worldSpacePivot=unity_ObjectToWorld._m03_m13_m23;// 樞軸和相機(jī)之間的偏移量
float3 worldSpacePivotToCamera=_WorldSpaceCameraPos.xyz - worldSpacePivot;// 相機(jī)向上向量
// 用作一個(gè)相當(dāng)任意的向上方向起點(diǎn)
float3 up=UNITY_MATRIX_I_V._m01_m11_m2;// 前進(jìn)向量是歸一化的偏移量
// 這是從樞軸到相機(jī)的方向
float3 forward=normalize(worldSpacePivotToCamera);// 叉積獲取一個(gè)垂直于輸入向量的向量
float3 right=normalize(cross(forward, up));// 另一個(gè)叉積確保向上向量垂直于兩者
up=cross(right, forward);// 構(gòu)建旋轉(zhuǎn)矩陣
float3x3 rotMat=float3x3(right, up, forward);// 上面的旋轉(zhuǎn)矩陣是轉(zhuǎn)置的,這意味著組件是
// 順序錯(cuò)誤,但我們可以通過(guò)交換
// 矩陣和向量在 mul() 中的順序來(lái)解決
float3 worldPos=mul(v.vertex.xyz, rotMat) + worldSpacePivot;// 光線方向
float3 worldRayDir=worldPos - _WorldSpaceCameraPos.xyz;
o.rayDir=mul(unity_WorldToObject, float4(worldRayDir, 0.0));// 剪切空間位置輸出
o.pos=UnityWorldToClipPos(worldPos);
這更好,但仍然不好。球體仍然剪切了四邊形的邊緣。實(shí)際上,現(xiàn)在是所有四個(gè)邊緣。至少它是居中的。好吧,我們忘記將四邊形移向相機(jī)了!從技術(shù)上講,我們也可以按任意量縮放四邊形,但讓我們回到這一點(diǎn)。
float3 worldPos=mul(float3(v.vertex.xy, 0.3), rotMat) + worldSpacePivot;
我們忽略了四邊形的 z,并添加了一個(gè)小的(任意的)偏移量以將其拉向相機(jī)。與任意縮放相比,這種選擇的好處是,當(dāng)距離較遠(yuǎn)時(shí),它應(yīng)該更緊密地限制在球體的邊界內(nèi),并且當(dāng)距離較近時(shí),由于透視變化而進(jìn)行縮放,就像球體本身一樣。只有當(dāng)非常靠近時(shí),它才會(huì)開(kāi)始覆蓋比需要更多的屏幕空間。我在上面的示例中選擇了 0.3,因?yàn)樗窃诳拷鼤r(shí)不會(huì)覆蓋太多屏幕空間,同時(shí)仍然覆蓋所有可見(jiàn)球體,直到你非常非常靠近。
你知道,你可能可以用一些數(shù)學(xué)方法來(lái)計(jì)算出在給定距離下拉動(dòng)或縮放四邊形的確切值……
等等!我們可以用一些數(shù)學(xué)方法來(lái)計(jì)算出這個(gè)值!我們可以計(jì)算出相機(jī)到樞軸向量和相機(jī)到球體可見(jiàn)邊緣之間的角度。事實(shí)上,它始終是一個(gè)直角三角形,直角位于球體的表面!還記得你老朋友 SOHCAHTOA 嗎?我們知道相機(jī)到樞軸的距離,那是斜邊。我們也知道球體的半徑。由此,我們可以計(jì)算出從將該角度投影到四邊形的平面所形成的直角三角形的底邊。有了它,我們可以縮放四邊形,而不是修改 v.vertex.z。
// 獲取直角三角形的正弦值,斜邊是 // 球體樞軸距離,對(duì)邊使用球體半徑
float sinAngle=0.5 / length(viewOffset);// 轉(zhuǎn)換為余弦
float cosAngle=sqrt(1.0 - sinAngle * sinAngle);// 轉(zhuǎn)換為正切
float tanAngle=sinAngle / cosAngle;// 上面的兩行等效于此,但速度更快
// tanAngle=tan(asin(sinAngle));// 獲取直角三角形對(duì)邊,直角位于球體樞軸處,乘以 2 以獲取四邊形大小
float quadScale=tanAngle * length(viewOffset) * 2.0;// 按計(jì)算的大小縮放四邊形
float3 worldPos=mul(float3(v.vertex.xy, 0.0) * quadScale, rotMat) + worldSpacePivot;
在這篇文章的開(kāi)頭,我們將所有內(nèi)容轉(zhuǎn)換為使用物體空間,這樣我們就可以輕松地支持旋轉(zhuǎn)和縮放。我們?nèi)匀恢С中D(zhuǎn),因?yàn)樗倪呅蔚某驅(qū)嶋H上并不重要。但四邊形不會(huì)像立方體那樣隨著物體的變換進(jìn)行縮放。解決這個(gè)問(wèn)題最簡(jiǎn)單的方法是從變換矩陣的軸中提取縮放比例,并將我們使用的半徑乘以最大縮放比例。
// 獲取物體縮放比例
float3 scale=float3(
length(unity_ObjectToWorld._m00_m10_m20),
length(unity_ObjectToWorld._m01_m11_m21),
length(unity_ObjectToWorld._m02_m12_m22)
);
float maxScale=max(abs(scale.x), max(abs(scale.y), abs(scale.z)));// 將球體半徑乘以最大縮放比例
float maxRadius=maxScale * 0.5;// 使用新的半徑更新我們的正弦計(jì)算
float sinAngle=maxRadius / length(viewOffset);// 執(zhí)行其余的縮放代碼
現(xiàn)在你可以均勻地縮放游戲物體,球體仍然會(huì)完美地限制在四邊形內(nèi)。
也應(yīng)該可以計(jì)算出橢球體或非均勻縮放球體的精確邊界。不幸的是,這開(kāi)始變得有點(diǎn)困難了。所以我現(xiàn)在不會(huì)花精力去解決這個(gè)問(wèn)題。我將把它留作“讀者的練習(xí)”。(也就是說(shuō),我不知道怎么做。)
使用四邊形的另一個(gè)問(wèn)題是 Unity 的視錐體剔除。它不知道四邊形在著色器中被旋轉(zhuǎn)了,因此,如果游戲物體被旋轉(zhuǎn),使其以邊緣朝向觀察者,它可能會(huì)被視錐體剔除,而球體仍然可見(jiàn)。解決這個(gè)問(wèn)題的方法是使用一個(gè)自定義的四邊形網(wǎng)格,其邊界已通過(guò) C# 代碼手動(dòng)修改為一個(gè)盒子。或者,你可以使用一個(gè)四邊形網(wǎng)格,其中一個(gè)頂點(diǎn)向前推了 0.5,另一個(gè)頂點(diǎn)向后推了 0.5,位于 z 軸上。我們已經(jīng)在著色器中通過(guò)用 0.0 替換 v.vertex.z 來(lái)展平網(wǎng)格。
所以現(xiàn)在我們得到了一個(gè)漂亮渲染的球體,它位于一個(gè)四邊形上,可以被照亮、紋理化,并且可以移動(dòng)、縮放和旋轉(zhuǎn)。所以讓我們讓它投射陰影!為此,我們需要在著色器中創(chuàng)建一個(gè)陰影投射器通道。幸運(yùn)的是,相同的頂點(diǎn)著色器可以在這兩個(gè)通道中重復(fù)使用,因?yàn)樗粍?chuàng)建了一個(gè)四邊形,并將光線起點(diǎn)和方向傳遞下去。當(dāng)然,這些對(duì)于陰影來(lái)說(shuō)與相機(jī)完全相同,對(duì)吧?然后,片段著色器實(shí)際上只需要輸出深度,這樣你就可以刪除所有討厭的 UV 和光照代碼。
哦。
光線起點(diǎn)和方向需要來(lái)自光源,而不是相機(jī)。我們用來(lái)表示光線起點(diǎn)的值始終是當(dāng)前相機(jī)位置,而不是光源。好消息是,這并不難修復(fù)。我們可以用 UNITY_MATRIX_I_V._m03_m13_m23 替換任何對(duì) _WorldSpaceCameraPos 的使用,它從逆視圖矩陣中獲取當(dāng)前視圖的世界位置。現(xiàn)在,只要陰影是用透視投影渲染的,它就應(yīng)該可以正常工作!
哦。哦,不。
方向陰影使用正交投影。
透視投影和光線追蹤的優(yōu)點(diǎn)是,光線起點(diǎn)位于相機(jī)的位置。這很容易獲得,即使對(duì)于任意視圖也是如此,如上所示。對(duì)于正交投影,光線方向是前進(jìn)視圖向量。這很容易從逆視圖矩陣中再次獲得。
// 視圖空間中的前進(jìn)方向是 -z,所以我們想要負(fù)向量
float3 worldSpaceViewForward=-UNITY_MATRIX_I_V._m02_m12_m22;
但是我們?nèi)绾潍@得正交光線起點(diǎn)呢?如果你嘗試在線搜索,你可能會(huì)看到很多示例使用 C# 腳本來(lái)獲取逆投影矩陣。或者濫用當(dāng)前的 unity_OrthoParams,它包含有關(guān)正交投影的寬度和高度的信息。然后,你可以使用剪切空間位置來(lái)重建光線起源的近視平面位置。這些方法的問(wèn)題在于,它們都獲取的是相機(jī)的正交設(shè)置,而不是當(dāng)前光源的設(shè)置。所以我們必須在著色器中計(jì)算逆矩陣!
float4x4 inverse(float4x4 m) {
float n11=m[0][0], n12=m[1][0], n13=m[2][0], n14=m[3][0];
float n21=m[0][1], n22=m[1][1], n23=m[2][1], n24=m[3][1];
float n31=m[0][2], n32=m[1][2], n33=m[2][2], n34=m[3][2];
float n41=m[0][3], n42=m[1][3], n43=m[2][3], n44=m[3][3]; float t11=n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44;// ... 等等,還有多少行?
好吧,我們不要這樣做。這些只是超過(guò) 30 行函數(shù)的前幾行,而且越來(lái)越長(zhǎng),越來(lái)越復(fù)雜。一定有更好的方法。
事實(shí)證明,你不需要任何這些。我們實(shí)際上并不需要光線起點(diǎn)位于近平面。光線起點(diǎn)實(shí)際上只需要是沿著前進(jìn)視圖向量拉回的網(wǎng)格位置。只要足夠遠(yuǎn),以確保它沒(méi)有從球體的體積內(nèi)部開(kāi)始。至少假設(shè)相機(jī)本身還沒(méi)有位于球體內(nèi)部。并且相機(jī)位置處的“近平面”而不是實(shí)際的近平面完全符合這個(gè)要求。
我們已經(jīng)在頂點(diǎn)著色器中知道了頂點(diǎn)的世界位置。所以我們可以將世界位置轉(zhuǎn)換為視圖空間。將 viewSpacePos.z 設(shè)置為零,然后轉(zhuǎn)換回世界空間。這將產(chǎn)生一個(gè)可用于正交投影的光線起點(diǎn)!
// 將世界空間頂點(diǎn)位置轉(zhuǎn)換為視圖空間
float4 viewSpacePos=mul(UNITY_MATRIX_V, float4(worldPos, 1.0));// 將視圖空間位置展平到相機(jī)平面上
viewSpacePos.z=0.0;// 轉(zhuǎn)換回世界空間
float4 worldRayOrigin=mul(UNITY_MATRIX_I_V, viewSpacePos);// 正交光線 dir
float3 worldRayDir=worldSpaceViewForward;// 以及到物體空間
o.rayDir=mul(unity_WorldToObject, float4(worldRayDir, 0.0));
o.rayOrigin=mul(unity_WorldToObject, worldRayOrigin);
實(shí)際上,我們甚至不需要做所有這些。還記得上面提到的 dot() 的超級(jí)能力嗎?我們只需要相機(jī)到頂點(diǎn)位置向量和歸一化的前進(jìn)視圖向量。我們已經(jīng)有了相機(jī)到頂點(diǎn)位置向量,那是原始的透視世界空間光線方向。我們知道前進(jìn)視圖向量,可以通過(guò)從上面提到的矩陣中提取它來(lái)獲得。方便的是,此向量已經(jīng)歸一化了!所以我們可以刪除上面的代碼中的兩個(gè)矩陣乘法,并改為執(zhí)行以下操作:
float3 worldSpaceViewPos=UNITY_MATRIX_I_V._m03_m13_m23;
float3 worldSpaceViewForward=-UNITY_MATRIX_I_V._m02_m12_m22;// 原始的透視光線 dir
float3 worldCameraToPos=worldPos - worldSpaceViewPos;// 正交光線 dir
float3 worldRayDir=worldSpaceViewForward * -dot(worldCameraToPos, worldSpaceViewForward);// 正交光線起點(diǎn)
float3 worldRayOrigin=worldPos - worldRayDir;o.rayDir=mul(unity_WorldToObject, float4(worldRayDir, 0.0));
o.rayOrigin=mul(unity_WorldToObject, float4(worldRayOrigin, 1.0));
** 這里有一個(gè)小問(wèn)題。這對(duì)于傾斜投影(即剪切的正交投影)不起作用。為此,你確實(shí)需要逆投影矩陣。但是剪切的透視投影是可以的!*
還記得我們是如何做面向相機(jī)的公告牌的嗎?以及用于縮放四邊形以考慮透視的那些花哨的數(shù)學(xué)方法嗎?對(duì)于正交投影,我們不需要任何這些。只需要執(zhí)行面向視圖的公告牌,并將四邊形按物體的變換的最大縮放比例進(jìn)行縮放。但是也許我們不要?jiǎng)h除所有這些代碼。我們可以照常使用現(xiàn)有的旋轉(zhuǎn)矩陣構(gòu)建,只是將 forward 向量更改為負(fù)的 worldSpaceViewForward 向量,而不是 worldSpacePivotToCamera 向量。
事實(shí)上,現(xiàn)在可能是討論聚光燈和點(diǎn)光源如何使用透視投影的好時(shí)機(jī)。如果我們想要支持方向光、聚光燈和點(diǎn)光源陰影,我們需要在同一個(gè)著色器中同時(shí)支持透視和正交投影。Unity 還使用此通道來(lái)渲染相機(jī)深度紋理。這意味著我們需要檢測(cè)當(dāng)前投影矩陣是否是正交的,并在兩種路徑之間進(jìn)行選擇。
好吧,我們可以通過(guò)檢查投影矩陣的特定組件來(lái)找出我們正在使用哪種類型的投影矩陣。如果投影矩陣的最后一個(gè)組件是 0.0,則它是透視投影矩陣,如果它是 1.0,則它是正交投影矩陣。
bool isOrtho=UNITY_MATRIX_P._m33==1.0;// 公告牌代碼
float3 forward=isOrtho ? -worldSpaceViewForward : normalize(worldSpacePivotToCamera);
// 執(zhí)行其余的公告牌代碼// 四邊形縮放代碼
float quadScale=maxScale;
if (!isOrtho)
{
// 執(zhí)行完美的縮放代碼
}// 光線方向和起點(diǎn)代碼
float3 worldRayOrigin=worldSpaceViewPos;
float3 worldRayDir=worldPos - worldSpaceRayOrigin;
if (isOrtho)
{
worldRayDir=worldSpaceViewForward * -dot(worldRayDir, worldSpaceViewForward);
worldRayOrigin=worldPos - worldRayDir;
}o.rayDir=mul(unity_WorldToObject, float4(worldRayDir, 0.0));
o.rayOrigin=mul(unity_WorldToObject, float4(worldRayOrigin, 1.0));// 不要擔(dān)心,我稍后會(huì)展示整個(gè)頂點(diǎn)著色器
現(xiàn)在,我們得到了一個(gè)可以正確處理正交投影和透視投影的頂點(diǎn)函數(shù)!片段著色器中不需要更改任何內(nèi)容來(lái)考慮這一點(diǎn)。哦,我們實(shí)際上可以使用同一個(gè)函數(shù)來(lái)表示陰影投射器通道和前向照明通道。現(xiàn)在,你也可以使用正交相機(jī)了!
現(xiàn)在,如果你一直在關(guān)注,你將得到一個(gè)輸出深度的陰影投射器通道。但我們沒(méi)有調(diào)用陰影投射器通常用于應(yīng)用偏移的任何常用函數(shù)。目前,這并不明顯,因?yàn)槲覀冞€沒(méi)有進(jìn)行自陰影,但如果我們不修復(fù)它,這將是一個(gè)問(wèn)題。
我們不會(huì)使用內(nèi)置的 TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) 宏來(lái)表示頂點(diǎn)著色器,因?yàn)槲覀冃枰谄沃髦羞M(jìn)行偏差。幸運(yùn)的是,在物體空間中進(jìn)行光線追蹤還有另一個(gè)好處。陰影投射器頂點(diǎn)著色器宏調(diào)用的第一個(gè)函數(shù)假設(shè)傳遞給它的位置位于物體空間中!我的意思是,這是有道理的,因?yàn)樗僭O(shè)它正在處理起始的物體空間頂點(diǎn)位置。但這意味著我們可以直接使用陰影投射器宏調(diào)用的偏差函數(shù),使用我們光線追蹤的位置,它們就會(huì)正常工作!
是的,實(shí)際上仍然只是一個(gè)四邊形。
Tags { "LightMode"="ShadowCaster" }ZWrite On ZTest LEqualCGPROGRAM
#pragma vertex vert
#pragma fragment frag_shadow#pragma multi_compile_shadowcaster// 是的,我知道頂點(diǎn)函數(shù)缺失fixed4 frag_shadow (v2f i,
out float outDepth : SV_Depth
) : SV_Target
{
// 光線起點(diǎn)
float3 rayOrigin=i.rayOrigin; // 歸一化光線向量
float3 rayDir=normalize(i.rayDir); // 光線球體相交
float rayHit=sphIntersect(rayOrigin, rayDir, float4(0,0,0,0.5)); // 上面的函數(shù)在沒(méi)有相交時(shí)返回 -1
clip(rayHit); // 計(jì)算物體空間位置
float3 objectSpacePos=rayDir * rayHit + rayOrigin; // 輸出修改后的深度
// 是的,我們將 objectSpacePos 作為兩個(gè)參數(shù)傳遞
// 第二個(gè)用于物體空間法線,在本例中
// 是歸一化的位置,但該函數(shù)將其轉(zhuǎn)換為
// 世界空間并進(jìn)行歸一化,所以我們不必這樣做
float4 clipPos=UnityClipSpaceShadowCasterPos(objectSpacePos, objectSpacePos);
clipPos=UnityApplyLinearShadowBias(clipPos);
outDepth=clipPos.z / clipPos.w; return 0;
}
ENDCG
就是這樣。這適用于所有陰影投射器變體。方向光陰影、聚光燈陰影、點(diǎn)光源陰影以及相機(jī)深度紋理!你知道,如果我們想支持多個(gè)燈光……
** 我沒(méi)有添加對(duì) GLES 2.0 點(diǎn)光源陰影的支持。這需要將距離光源的距離作為陰影投射器通道的顏色值輸出,而不是僅僅硬編碼 *0*。添加它并不難,但這會(huì)使著色器變得更加混亂,因?yàn)樾枰砑右恍?*#if* 和我們需要計(jì)算的特殊情況數(shù)據(jù)。所以我沒(méi)有包含它。*
** 編輯:我忘記了在處理 OpenGL 平臺(tái)上的深度時(shí)的一件事。OpenGL 的剪切空間 z 是 -w 到 +w 的范圍,所以你需要執(zhí)行一個(gè)額外的步驟將其轉(zhuǎn)換為片段著色器輸出深度所需的 0.0 到 1.0 的范圍。*
#if !defined(UNITY_REVERSED_Z) // 基本上只有 OpenGL
outDepth=outDepth * 0.5 + 0.5;
#endif
所以現(xiàn)在我們得到了一個(gè)有效的陰影投射。那么陰影接收呢?這將進(jìn)入 Unity 特定內(nèi)容的陰暗面。如果你不是凡人,現(xiàn)在就轉(zhuǎn)身吧……或者,如果你不太關(guān)心 Unity 的內(nèi)置前向渲染路徑。(或者至少跳到下一節(jié)關(guān)于 深度 的內(nèi)容。)
在早期,我發(fā)布了一個(gè)帶有基本漫反射光照設(shè)置的著色器。如果你一直關(guān)注這篇文章,那么前向基本通道的光照代碼現(xiàn)在應(yīng)該看起來(lái)像這樣。
// 世界空間表面法線和位置
float3 worldNormal=UnityObjectToWorldNormal(objectSpaceNormal);
float3 worldPos=mul(unity_ObjectToWorld, float4(objectSpacePos, 1.0));// 基本光照
half3 worldLightDir=UnityWorldSpaceLightDir(worldPos);
half ndotl=saturate(dot(worldNormal, worldLightDir));
half3 lighting=_LightColor0 * ndotl;// 環(huán)境光照
half3 ambient=ShadeSH9(float4(worldNormal, 1));
lighting +=ambient;// 應(yīng)用光照
col.rgb *=lighting;
沒(méi)什么特別的。獲取你的世界法線和世界位置。獲取世界光線方向。執(zhí)行一個(gè)鉗位點(diǎn)積。將光線顏色乘以點(diǎn)積,添加環(huán)境光照,并將紋理乘以光照。這有點(diǎn)像你開(kāi)始學(xué)習(xí)光照著色器教程時(shí)的代碼。但我們顯然缺少陰影。
對(duì)于傳統(tǒng)的向前基本照明著色器,我們想要在一些地方添加一些宏,Unity 會(huì)自動(dòng)為我們提供所需的內(nèi)容。將 SHADOW_COORDS(#) 添加到 v2f 結(jié)構(gòu)體中,在頂點(diǎn)函數(shù)中調(diào)用 TRANSFER_SHADOW(o);,然后在片段著色器中調(diào)用 UNITY_LIGHT_ATTENUATION(atten, i, worldPos);。我們當(dāng)然可以這樣做,至少對(duì)于向前基本通道來(lái)說(shuō)可以這樣做。在桌面和主機(jī)上,Unity 的方向光的陰影使用屏幕空間陰影。也就是說(shuō),陰影貼圖被渲染,然后它們被投射到從相機(jī)深度紋理中事先計(jì)算出的世界位置上,并保存在屏幕空間紋理中。所以上面的宏只是將屏幕空間位置傳遞下去,你可以從剪切空間位置中廉價(jià)地計(jì)算出它。
通常,這是通過(guò)上面提到的 TRANSFER_SHADOW(o); 宏來(lái)完成的,并從頂點(diǎn)著色器傳遞到片段著色器。但我們已經(jīng)在片段著色器中計(jì)算了剪切空間位置。我們可以重復(fù)使用它,使用宏調(diào)用的同一個(gè) ComputeScreenPos(clipPos) 函數(shù)來(lái)計(jì)算屏幕空間位置。然后,我們可以使用最終的內(nèi)置宏,讓它完成剩下的工作。
我們確實(shí)想要使用 UNITY_LIGHT_ATTENUATION(atten, i, worldPos); 宏。它為我們處理額外的功能,例如光線餅干。以及另一個(gè)我將在稍后提到的原因。
但有一個(gè)小問(wèn)題。內(nèi)置的陰影宏期望你傳遞一個(gè)包含屏幕空間位置的結(jié)構(gòu)體。而我們的 v2f 結(jié)構(gòu)體沒(méi)有它,如果我們不必這樣做,我們也不想把它添加到該結(jié)構(gòu)體中。
謝天謝地,我們不需要這樣做,我們可以創(chuàng)建一個(gè)虛擬結(jié)構(gòu)體!它只需要 SHADOW_COORDS(0) 宏來(lái)添加其他宏期望的結(jié)構(gòu)體元素,然后我們就可以自己設(shè)置它添加的值。
// 虛擬結(jié)構(gòu)體
struct shadowInput {
SHADOW_COORDS(0)
);// 世界空間位置和剪切空間位置
float3 worldPos=mul(unity_ObjectToWorld, float4(surfacePos, 1.0));
float4 clipPos=UnityWorldToClipPos(float4(worldPos, 1.0));#if defined (SHADOWS_SCREEN)
// 為屏幕空間陰影設(shè)置陰影結(jié)構(gòu)體
shadowInput shadowIN;
#if defined(UNITY_NO_SCREENSPACE_SHADOWS)
// 移動(dòng)陰影
shadowIN._ShadowCoord=mul(unity_WorldToShadow[0], float4(worldPos, 1.0));
#else
// 屏幕空間陰影
shadowIN._ShadowCoord=ComputeScreenPos(clipPos);
#endif // UNITY_NO_SCREENSPACE_SHADOWS
#else
float shadowIN=0;
#endif // SHADOWS_SCREEN// 宏創(chuàng)建一個(gè)名為 atten 的變量,其中包含陰影
UNITY_LIGHT_ATTENUATION(atten, shadowIN, worldPos);// 將方向光照乘以 atten
half3 lighting=_LightColor0 * ndotl * atten;
現(xiàn)在,我們可以接收方向陰影了!
捕捉陰影。
所以我說(shuō)過(guò)我們確實(shí)想要使用上面的 UNITY_LIGHT_ATTENUATION 宏。這是真正的原因。它還處理其他燈光類型!Unity 的內(nèi)置前向渲染器通過(guò)為每個(gè)燈光再次渲染物體來(lái)繪制多個(gè)燈光。所以我們需要一個(gè)前向添加通道。而我們現(xiàn)在用于前向基本通道的唯一的阻止它與前向添加通道一起工作的東西是環(huán)境光照。所以你可以復(fù)制片段著色器函數(shù)并刪除兩行環(huán)境光照代碼。
或者,你可以在 #if defined(UNITY_SHOULD_SAMPLE_SH) 中放置這三行環(huán)境光照代碼,它只對(duì)基本通道為真。然后,你可以為這兩個(gè)通道共享完全相同的函數(shù)。
RTX 關(guān)閉!
使用 SV_Depth 有一個(gè)很大的問(wèn)題。它禁用了早期深度拒絕。基本上,這意味著如果你在視錐體中,你將支付渲染Imposter的成本。即使它位于其他東西的后面,并且不可見(jiàn)。通常,GPU 可以使用深度緩沖區(qū)來(lái)跳過(guò)對(duì)位于相機(jī)更近的其他物體后面的網(wǎng)格運(yùn)行片段著色器。但由于 GPU 在片段著色器運(yùn)行之后才知道深度是多少,因此它無(wú)法做到這一點(diǎn)。
“那么 SV_DepthLessEqual 或 SV_DepthGreaterEqual 呢?”
是的!這是一個(gè)很棒的問(wèn)題,佩蒂尼奧先生。你怎么知道(https://mynameismjp.wordpress.com/2010/11/14/d3d11-features/) 我在想這個(gè)?
SV_DepthLessEqual 和 SV_DepthGreaterEqual 語(yǔ)義是 SV_Depth 的替代品,它們告訴 GPU 仍然執(zhí)行早期深度拒絕,這是為著色器模型 5.0 添加的。但是要使用它,我們必須確保網(wǎng)格比我們要渲染的球體更靠近或更靠近相機(jī)。為此,我們想要將網(wǎng)格拉向相機(jī)。現(xiàn)在,面向相機(jī)的四邊形位于球體的中心。
問(wèn)題是我們需要將頂點(diǎn)移近相機(jī),而不會(huì)修改它們的屏幕空間位置。我們已經(jīng)為它們計(jì)算出了完美的邊界,所以如果我們最終取消了這些操作,那就很不幸了。
一個(gè)選擇是計(jì)算比球體樞軸更靠近相機(jī) maxRadius 的視平面的剪切空間位置。然后替換已經(jīng)計(jì)算出的剪切空間位置的 z。剪切空間有一個(gè)非常酷的功能,你可以更改剪切空間位置的 z,而不會(huì)影響它在屏幕上的位置或?qū)е虏逯祮?wèn)題。
// 著色器末尾的常用剪切空間
o.pos=UnityWorldToClipPos(worldPos);// 獲取球體樞軸沿 // 前進(jìn)視圖向量更靠近 `maxRadius` 的位置
float4 nearerClip=UnityWorldToClipPos(worldSpacePivotPos — worldSpaceViewForward * maxRadius);// 轉(zhuǎn)換應(yīng)用“透視除法”以獲取真實(shí)的深度 Z
float nearerZ=nearerClip.z / nearerClip.w// 用新的值替換原始的剪切空間 z
o.pos.z=nearerZ * o.pos.w;
但這種技術(shù)有一個(gè)很大的缺陷。如果你將相機(jī)移得太靠近或試圖穿過(guò)我們的Imposter球體,那么當(dāng)我們應(yīng)該仍然看到它時(shí),它就會(huì)消失。問(wèn)題是“更近的深度”被放置在相機(jī)的后面。我們可以嘗試對(duì)此進(jìn)行更多工作。例如,嘗試將 z 限制為近平面。或者,更確切地說(shuō),是將 z 限制在近剪切平面的內(nèi)部,因?yàn)?em>在近剪切平面上仍然會(huì)導(dǎo)致它被剔除。
// 限制為近剪切平面的內(nèi)部
o.pos.z=min(o.pos.w - 1.0e-6f, nearerZ * o.pos.w);
但……這實(shí)際上并沒(méi)有按預(yù)期工作*。
當(dāng)我 說(shuō)你可以更改剪切空間位置的 z 而不會(huì)出現(xiàn)任何問(wèn)題時(shí),我撒了點(diǎn)謊。這在一種情況下會(huì)失敗,那就是當(dāng)網(wǎng)格的某些頂點(diǎn)位于相機(jī)后面時(shí)。我們?cè)噲D解決的正是這種情況。即使進(jìn)行了鉗位,四邊形仍然比它應(yīng)該的更被剪切。所以這失敗了。
老實(shí)說(shuō),我不太了解這個(gè)問(wèn)題,無(wú)法解釋原因。
但有一個(gè)更便宜的解決方案,它在一般情況下表現(xiàn)良好,并且不會(huì)在“某些頂點(diǎn)位于相機(jī)后面”的情況下失敗!我們可以沿著光線方向?qū)㈨旤c(diǎn)移動(dòng)一個(gè)球體半徑。對(duì)于正交投影,這實(shí)際上只是世界位置減去前進(jìn)視圖乘以球體半徑。對(duì)于透視投影,如果我們使用歸一化的光線方向,它實(shí)際上不會(huì)拉得足夠遠(yuǎn)。所以我們需要再次調(diào)用我們的朋友 dot(),以找出我們需要偏移多遠(yuǎn)才能正確地將四邊形的表面拉近一個(gè)球體半徑。
// 這將頂點(diǎn)推向相機(jī)
// 在頂點(diǎn)著色器中的 UnityWorldToClipPos 行之前添加
worldPos +=worldSpaceRayDir / dot(normalize(viewOffset), worldSpaceRayDir) * maxRadius;// 著色器末尾的常用剪切空間
o.pos=UnityWorldToClipPos(worldPos);
現(xiàn)在,當(dāng)你的相機(jī)靠近時(shí),它仍然會(huì)與球體進(jìn)行近剪切,但結(jié)果與剪切實(shí)際球體網(wǎng)格非常相似。一般來(lái)說(shuō),如果網(wǎng)格沒(méi)有被剪切,那么光線偏移四邊形也不會(huì)被剪切。
添加了這一點(diǎn)之后,只需要將片段著色器中的 SV_Depth 語(yǔ)義替換為適當(dāng)?shù)倪x項(xiàng)。對(duì)于任何不是 OpenGL 的內(nèi)容,你應(yīng)該使用 SV_DepthLessEqual。這是因?yàn)?Unity 為非 OpenGL 平臺(tái)使用反向 Z 深度。反向 Z 深度意味著距離更遠(yuǎn)的物體具有比更近的物體更小的深度值。所以實(shí)際上,我們只需要檢查 UNITY_REVERSED_Z 關(guān)鍵字是否處于活動(dòng)狀態(tài)。對(duì)于 OpenGL……好吧,實(shí)際上這都是無(wú)用的。我們無(wú)法保證 OpenGL 平臺(tái)支持與 SV_DepthGreaterEqual 等效的功能,直到 OpenGL 4.2。 基本上,你可能被迫在任何不使用反向 Z 深度的平臺(tái)上使用 SV_Depth。然后,所有這些將四邊形拉近相機(jī)以減少過(guò)度陰影的操作對(duì)于這些平臺(tái)來(lái)說(shuō)都是毫無(wú)意義的。但我們至少可以在著色器中處理這兩種情況。
** 編輯:運(yùn)行 OpenGL 4.2+ 的 Unity 仍然使用常規(guī)的 z 深度。你可以為它使用 *SV_DepthGreaterEqual*,但實(shí)際上,任何支持 OpenGL 4.2 的平臺(tái),你都希望改為運(yùn)行 Direct3D、Vulkan 或 Metal。*
// 這樣更新片段著色器函數(shù)
half4 frag_(forward/shadow) (v2f i
#if UNITY_REVERSED_Z && SHADER_TARGET > 40
, out float outDepth : SV_DepthLessEqual
#else
// 該設(shè)備可能無(wú)法使用保守深度
, out float outDepth : SV_Depth
#endif
) : SV_Target
還有一些小細(xì)節(jié)需要完善著色器。支持“每個(gè)頂點(diǎn)”非重要燈光、霧和基本實(shí)例化。這些并不十分有趣,所以我將快速介紹一下。
由于我們實(shí)際上沒(méi)有很多頂點(diǎn),所以我們還需要在片段著色器中調(diào)用“頂點(diǎn)燈光”函數(shù)。這實(shí)際上只是復(fù)制和粘貼頂點(diǎn)燈光函數(shù),將其放在一個(gè) #if 中,并將返回值添加到 lighting 中。
#if defined(VERTEXLIGHT_ON)
// “每個(gè)頂點(diǎn)”非重要燈光
half3 vertexLighting=Shade4PointLights(
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb,
unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0,
worldPos, worldNormal);lighting +=vertexLighting;
#endif
或者至少它應(yīng)該這么簡(jiǎn)單。VERTEXLIGHT_ON 是由 #pragma multi_compile_fwdbase 控制的關(guān)鍵字之一。但似乎,如果你在頂點(diǎn)著色器中沒(méi)有這個(gè)函數(shù),那么具有該關(guān)鍵字的著色器變體將永遠(yuǎn)不會(huì)創(chuàng)建。所以你必須用自己的多編譯行來(lái)強(qiáng)制執(zhí)行它。
#pragma multi_compile _ VERTEXLIGHT_ON
與這篇文章中介紹的許多內(nèi)容一樣,Unity 的內(nèi)置宏假設(shè)你正在從頂點(diǎn)著色器中輸出某種值。對(duì)于桌面,這只是將原始的 clipPos.z 傳遞給片段著色器,然后片段著色器在調(diào)用那里的霧宏時(shí)計(jì)算實(shí)際的霧衰減。所以,我們可以在前向通道的片段著色器末尾添加帶有 UNITY_APPLY_FOG(clipPos.z, col); 的常用宏。
對(duì)于移動(dòng)設(shè)備,衰減是在頂點(diǎn)著色器中計(jì)算的。但我們需要使用我們?cè)谄沃髦杏?jì)算的 clipPos.z,所以如果你想要同時(shí)支持移動(dòng)設(shè)備和桌面,我們不能只使用常用的 UNITY_APPLY_FOG(clipPos.z, col) 宏。所以我們必須計(jì)算衰減并將它傳遞給宏,但只在移動(dòng)設(shè)備上這樣做。
// 霧
float fogCoord=clipPos.z;#if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE)
// 宏計(jì)算霧衰減
// 并創(chuàng)建一個(gè) unityFogFactor 變量來(lái)保存它
UNITY_CALC_FOG_FACTOR(fogCoord);
fogCoord=unityFogFactor;
#endifUNITY_APPLY_FOG(fogCoord, col);
要將實(shí)例化添加到著色器中,請(qǐng)復(fù)制和粘貼 Unity 關(guān)于此內(nèi)容的文檔中提到的適當(dāng)宏:
https://docs.unity3d.com/Manual/GPUInstancing.html
轉(zhuǎn)到將實(shí)例化添加到頂點(diǎn)和片段著色器部分,并將宏復(fù)制到 appdata 和 v2f 結(jié)構(gòu)體、頂點(diǎn)函數(shù)以及片段函數(shù)中。忽略 BUFFER 和 PROP 宏。但你確實(shí)需要在片段著色器中使用 UNITY_SETUP_INSTANCE_ID(i);。在實(shí)例化著色器中,unity_ObjectToWorld 和 unity_WorldToObject 矩陣是實(shí)例化屬性。由于我們?cè)谄沃髦惺褂盟鼈儯虼宋覀円残枰獙?shí)例 ID。
話不多說(shuō),這是完成的著色器,完整代碼如下。
完整代碼(https://gist.github.com/bgolus/1188cd89968b977d5c468bf7bbb3250b)
因?yàn)槲抑老乱粋€(gè)問(wèn)題每個(gè)人都會(huì)問(wèn)的是“如何在表面著色器/著色器圖中做這個(gè)?”。以下是這些問(wèn)題的答案。
你不能。*
好吧,你可以構(gòu)建光線起點(diǎn)和方向。你可以進(jìn)行球體的光線追蹤。你當(dāng)然也可以執(zhí)行所有過(guò)程式 UV 操作。你甚至可以更新表面法線,使其像球體一樣被照亮。
你不能做的一件事是從片段著色器中調(diào)整用于光照和陰影的深度或世界位置。因此,深度相交看起來(lái)會(huì)很奇怪,陰影看起來(lái)會(huì)很奇怪,并且非常靠近表面的燈光看起來(lái)也不正確。因?yàn)樗鼈兌紝⑹褂迷季W(wǎng)格表面的位置。
因此,在 Unity 的任何渲染器中使用這種技術(shù)的唯一選擇是使用手寫的頂點(diǎn)片段著色器。至少目前是這樣。我希望有一天你能夠在著色器圖中輸出修改后的深度值。但截至撰寫本文時(shí),他們還沒(méi)有提到要添加此功能。
** 人們指出,HDRP 的著色器圖確實(shí)具有在主節(jié)點(diǎn)上設(shè)置深度以執(zhí)行每個(gè)片段深度功能的能力。不過(guò),它使用的是 *SV_Depth* 而不是 *SV_DepthLessEqual*,因此不需要執(zhí)行四邊形的射線方向偏移。感謝 Rémy 提醒我。希望他們能將此功能添加到 URP 中。
https://portal.productboard.com/unity/1-unity-graphics/tabs/7-shader-graph
我的許多其他文章都是關(guān)于抗鋸齒的,為什么我在這里跳過(guò)了它?因?yàn)檫@是一個(gè)沒(méi)有完美解決方案的難題。
Inigo Quiles 在這里有一個(gè)關(guān)于如何處理光線追蹤球體的抗鋸齒的優(yōu)秀示例:
https://www.shadertoy.com/view/MsSSWV
基本原理是使用光線到點(diǎn)距離計(jì)算(這也用于修復(fù)外部邊緣的 UV),以近似地了解光線在屏幕空間中距離球體邊緣有多近。這可以為你提供一個(gè)漸變,可以使用類似于我在 Alpha to Coverage 文章中使用的函數(shù)來(lái)銳化,然后將其用作輸出 alpha。也可以用于非 MSAA 和非不透明用例中的 alpha 混合。
使用原始著色器的 4x MSAA 與使用 Alpha to Coverage 的比較。
// 將此添加到通道中,位于 CGPROGRAM 之外,以啟用
// alpha to coverage
AlphaToMask On
// 光線到球體樞軸距離
float rayToPointDist=length(rayDir * dot(rayDir, -rayOrigin) + rayOrigin);// fwidth 獲取 ddx 和 ddy 偏導(dǎo)數(shù)的總和
// float fDist=fwidth(rayToPointDist);// fwidth 是對(duì)此的粗略近似
float fDist=length(float2(ddx(rayToPointDist), ddy(rayToPointDist)));// 銳化光線到點(diǎn)距離
// 以球體半徑為中心,根據(jù)導(dǎo)數(shù) +/- 半個(gè)像素
float alpha=(0.5 - rayToPointDist) / max(fDist, 0.0001) + 0.5;// 根據(jù)銳化的 alpha 剪切
// 不要根據(jù)光線命中未命中進(jìn)行剪切
clip(alpha);// 將 alpha 限制在 0 到 1 的范圍內(nèi),并在
// 采樣紋理后將其應(yīng)用于輸出 alpha
col.a=saturate(alpha);
這似乎應(yīng)該足夠好了,對(duì)吧?那么為什么我說(shuō)沒(méi)有完美的解決方案呢?為什么我沒(méi)有默認(rèn)實(shí)現(xiàn)它呢?對(duì)外部邊緣進(jìn)行抗鋸齒并不能解決與光柵化網(wǎng)格或從片段著色器輸出深度的其他著色器相交時(shí)的鋸齒問(wèn)題。當(dāng)啟用 MSAA 光柵化三角形時(shí),會(huì)為三角形覆蓋的每個(gè)子樣本計(jì)算深度,但片段著色器只對(duì)每個(gè)像素運(yùn)行一次。這意味著兩個(gè)相交網(wǎng)格的每個(gè)子樣本覆蓋可以準(zhǔn)確地確定到子樣本計(jì)數(shù)。此著色器正在從片段著色器中寫入深度,因此每個(gè)像素只有一個(gè)深度。然后,相同的深度用于所有子樣本。因此,相交處沒(méi)有 AA。從技術(shù)上講,在光柵化幾何體和輸出深度的片段著色器之間仍然存在一些 AA,因?yàn)闀?huì)考慮相交三角形的平面。但在兩個(gè)深度寫入著色器之間將不會(huì)存在任何 AA。
使用原始著色器的 4x MSAA 與使用 Alpha to Coverage 的比較。請(qǐng)注意,兩種方法的相交處都是相同的。與視平面對(duì)齊的光柵化表面在與Imposter相交處顯示鋸齒。以角度觀察的光柵化表面顯示抗鋸齒,但它等效于與視平面對(duì)齊的表面相交。
上面的 Shadertoy 示例可以處理相交,因?yàn)樗谝粋€(gè)通道中渲染所有這些球體,并對(duì)分析形狀執(zhí)行每個(gè)像素排序和合成。它甚至沒(méi)有執(zhí)行任何 MSAA。
據(jù)我所知,沒(méi)有一種有效的方法可以在啟用 MSAA 的情況下處理片段著色器深度寫入,同時(shí)仍然只對(duì)每個(gè)像素運(yùn)行一次片段著色器。這將導(dǎo)致使用 sample 插值修飾符來(lái)強(qiáng)制片段著色器對(duì)每個(gè)子樣本運(yùn)行。當(dāng) MSAA 的全部目的是不這樣做時(shí),這對(duì)于性能來(lái)說(shuō)并不理想。但它看起來(lái)確實(shí)很不錯(cuò)。
使用原始著色器的 4x MSAA 與強(qiáng)制每個(gè)子樣本渲染的著色器的比較。
使用原始著色器的 4x MSAA 與強(qiáng)制每個(gè)子樣本渲染的著色器的比較。請(qǐng)注意,超級(jí)采樣情況下的所有相交處都得到了適當(dāng)?shù)目逛忼X。
// 更新 v2f 結(jié)構(gòu)體以使用插值的 ray dir 和 ray origin 向量的樣本修飾符,以強(qiáng)制片段
// 著色器對(duì)每個(gè)子樣本運(yùn)行,并為插值
// 值獲取每個(gè)子樣本位置的唯一計(jì)算
struct v2f
{
float4 pos : SV_POSITION;
sample float3 rayDir : TEXCOORD0;
sample float3 rayOrigin : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
};// 將此添加到 CGPROGRAM 塊中,作為通道,因?yàn)?
// 樣本修飾符是著色器模型 5.0 的功能
#pragma target 5.0// 你可能還想對(duì)紋理 mip 層級(jí)進(jìn)行偏差
// 因?yàn)槿绻覀円呀?jīng)進(jìn)行了超級(jí)采樣,為什么不呢!
half4 col=tex2Dbias(_MainTex, float4(uv, 0, -1));
Alpha to Coverage 的 4x MSAA 與強(qiáng)制每個(gè)子樣本渲染的著色器的比較。
原始著色器的 4x MSAA 與 Alpha to Coverage 與強(qiáng)制超級(jí)采樣相交比較。
我沒(méi)有在示例著色器中包含延遲渲染通道。沒(méi)有理由認(rèn)為這不能與延遲渲染一起使用。它甚至?xí)菀拙帉憽5以噲D使著色器盡可能簡(jiǎn)單。
如果喜歡今天的文章,請(qǐng)多點(diǎn)點(diǎn)贊和在看,后續(xù)就會(huì)有更多此類的文章~
語(yǔ)義化標(biāo)簽,可以讓頁(yè)面有更加完善的結(jié)構(gòu),讓頁(yè)面的元素有含義,同時(shí)利于被搜索引擎解析,有利于SEO,主要標(biāo)簽包括下面的標(biāo)簽:
html5新的常用標(biāo)簽
②增強(qiáng)型表單
可以通過(guò)input的type屬性指定類型是number還是date或者url,同時(shí)還添加了placeholder和required等表單屬性。
<input type="range" id="a" value="50" required>
<input type="number" id="b" value="50" placeholder="請(qǐng)輸入數(shù)字">
③媒體元素
新增了audio和video兩個(gè)媒體相關(guān)的標(biāo)簽,可以讓開(kāi)發(fā)人員不必以來(lái)任何插件就能在網(wǎng)頁(yè)中嵌入瀏覽器的音頻和視頻內(nèi)容。
<video width="320" height="240" controls>
<source src="movie.mp4" type="video/mp4">
// 有些低版本瀏覽器不支持Video標(biāo)簽。
</video>
<audio controls>
<source src="horse.mp3" type="audio/mpeg">
// 有些低版本瀏覽器不支持 audio 元素。
</audio>
④canvas繪圖
canvas繪圖指的是在頁(yè)面中設(shè)定一個(gè)區(qū)域,然后通過(guò)JS動(dòng)態(tài)的在這個(gè)區(qū)域繪制圖形。
<canvas id="canvas" width="300" height="300"></canvas>
⑤svg繪圖
//畫了一個(gè)圓
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
</svg>
⑥地理定位
getCurrentPosition()方法來(lái)獲取用戶的位置,從而實(shí)現(xiàn)隊(duì)地理位置的定位。
var x=document.getElementById("demo");
function getLocation()
{
if (navigator.geolocation)
{
navigator.geolocation.getCurrentPosition(showPosition);
}
else
{
x.innerHTML="該瀏覽器不支持獲取地理位置。";
}
}
function showPosition(position)
{
x.innerHTML="緯度: " + position.coords.latitude +
"<br>經(jīng)度: " + position.coords.longitude;
}
⑦拖放API
通過(guò)給標(biāo)簽元素設(shè)置屬性draggable值為true,能夠?qū)崿F(xiàn)對(duì)目標(biāo)元素的拖動(dòng)。
<img draggable="true"> // 拖放圖片
⑧Web Worker
Web Worker通過(guò)加載一個(gè)腳本文件,進(jìn)而創(chuàng)建一個(gè)獨(dú)立工作的線程,在主線程之外運(yùn)行,worker線程運(yùn)行結(jié)束之后會(huì)把結(jié)果返回給主線程,worker線程可以處理一些計(jì)算密集型的任務(wù),這樣主線程就會(huì)變得相對(duì)輕松,這并不是說(shuō)JS具備了多線程的能力,而實(shí)瀏覽器作為宿主環(huán)境提供了一個(gè)JS多線程運(yùn)行的環(huán)境。
if(typeof(Worker)!=="undefined")
{
// 是的! Web worker 支持!
// 一些代碼.....
}
else
{
//抱歉! Web Worker 不支持
}
⑨Web Storage
需要重點(diǎn)掌握的是cookie、Localstorage和SessionStorage三者之間的區(qū)別:
1.有效期
2.存儲(chǔ)數(shù)據(jù)的大小
3.作用范圍
4.安全性
⑩Websocket
websocket和HTTP的區(qū)別:
【注】HTML5 定義的 WebSocket 協(xié)議,能更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實(shí)時(shí)地進(jìn)行通訊。
webSocket
學(xué)習(xí)記錄,如有侵權(quán)請(qǐng)聯(lián)系刪除
為一名前端愛(ài)好者, 我利用空余時(shí)間研究了幾個(gè)國(guó)外網(wǎng)站的源碼,發(fā)現(xiàn)不管是庫(kù),還是業(yè)務(wù)代碼,都會(huì)用到了一些比較有意思的API,雖然平時(shí)在工作中部分接觸過(guò),但是經(jīng)過(guò)這次的研究,覺(jué)得很有必要總結(jié)一下,畢竟已經(jīng)2020年了,是時(shí)候更新一下技術(shù)儲(chǔ)備了,本文主要通過(guò)實(shí)際案例來(lái)帶大家快速了解以下幾個(gè)知識(shí)點(diǎn):
我會(huì)對(duì)部分API做一些比較有意思的案例,那么開(kāi)始我們的學(xué)習(xí)吧~
Observer是瀏覽器自帶的觀察者,它主要提供了Intersection, Mutation, Resize, Performance這四類觀察者, 這里筆者重點(diǎn)介紹Intersection Observer.
IntersectionObserver提供了一種異步觀察目標(biāo)元素與其祖先元素交叉狀態(tài)的方法。當(dāng)一個(gè)IntersectionObserver對(duì)象被創(chuàng)建時(shí),其被配置為監(jiān)聽(tīng)根中一段給定比例的可見(jiàn)區(qū)域,并且無(wú)法更改其配置,所以一個(gè)給定的觀察者對(duì)象只能用來(lái)監(jiān)聽(tīng)可見(jiàn)區(qū)域的特定變化值;然而,我們可以在同一個(gè)觀察者對(duì)象中配置監(jiān)聽(tīng)多個(gè)目標(biāo)元素。
說(shuō)簡(jiǎn)單點(diǎn)就是該api可以異步監(jiān)聽(tīng)目標(biāo)元素在根元素里的位置變動(dòng),并觸發(fā)響應(yīng)事件.我們可以利用它來(lái)實(shí)現(xiàn)更為高效的圖片懶加載, 無(wú)限滾動(dòng)以及內(nèi)容埋點(diǎn)上報(bào)等.接下來(lái)我們通過(guò)一個(gè)例子來(lái)說(shuō)明一下它的使用步驟.
// 1.定義觀察者及觀察回調(diào)
const intersectionObserver=new IntersectionObserver((entries, observer)=> {
entries.forEach(entry=> {
console.log(entry)
// ...一些操作
});
},
{
root: document.querySelector('#root'),
rootMargin: '0px',
threshold: 0.5
}
)
// 2. 定義要觀察的目標(biāo)對(duì)象
const target=document.querySelector(“.target”);
intersectionObserver.observe(target);
以上代碼就實(shí)現(xiàn)了一個(gè)基本的Intersection Observer,雖然已有代碼中還體現(xiàn)不出什么實(shí)質(zhì)性功能. 接下來(lái)介紹一下代碼中使用到的參數(shù)的含義: * callback IntersectionObserver實(shí)例的第一個(gè)參數(shù), 當(dāng)目標(biāo)元素與根元素通過(guò)閾值 時(shí)就會(huì)觸發(fā)該回調(diào).回調(diào)中第一個(gè)參數(shù)是被觀察對(duì)象列表,一旦被觀察對(duì)象發(fā)生突變就會(huì)被移入該列表, 列表中每一項(xiàng)都保留有觀察者的位置信息;第二個(gè)參數(shù)為observer,觀察者本身.如下圖控制臺(tái)打印:
其中rootBounds表示根元素的位置信息, boundingClientRect表示目標(biāo)元素的位置信息,intersectionRect表示叉部分的位置信息, intersectionRatio表示目標(biāo)元素的可見(jiàn)比例.
當(dāng)我們?cè)O(shè)置rootMargin為10px時(shí),我們的root會(huì)增大影響范圍,但目標(biāo)元素移動(dòng)到淡紅色區(qū)域式就會(huì)被監(jiān)聽(tīng)到,當(dāng)然我們還可以設(shè)置rootMargin為負(fù)值來(lái)減少影響區(qū)域.其支持的值為百分比和px,如下:
rootMargin: '10px'
rootMargin: '10%'
rootMargin: '10px 0px 10px 10px'
thresholds可以如下圖理解:
由上圖所示,當(dāng)我們?cè)O(shè)置閾值為[0.25, 0.5]時(shí), 目標(biāo)元素的25%和50%進(jìn)入根元素的影響范圍時(shí)都會(huì)觸發(fā)回調(diào).利用這個(gè)特性我們往往可以實(shí)現(xiàn)位差動(dòng)畫,或者更根據(jù)目標(biāo)元素的位置變化做不同的交互. 當(dāng)然Intersection還提供了以下幾個(gè)方法來(lái)控制觀察對(duì)象: disconnect() 使IntersectionObserver對(duì)象停止監(jiān)聽(tīng)工作 takeRecords() 返回所有觀察目標(biāo)的IntersectionObserverEntry對(duì)象數(shù)組 * unobserve() 使IntersectionObserver停止監(jiān)聽(tīng)特定目標(biāo)元素
了解了使用方法和api之后,我們來(lái)看看一個(gè)實(shí)際應(yīng)用--實(shí)現(xiàn)圖片懶加載:
<img src="loading.gif" data-src="absolute.jpg">
<img src="loading.gif" data-src="relative.jpg">
<img src="loading.gif" data-src="fixed.jpg">
<script>
let observerImg=new IntersectionObserver(
(entries, observer)=> {
entries.forEach(entry=> {
// 替換為正式的圖片
entry.target.src=entry.target.dataset.src;
// 停止監(jiān)聽(tīng)
observer.unobserve(entry.target);
});
},
{
root: documennt.getElementById('scrollView'),
threshold: 0.3
}
);
document.querySelectorAll('img').forEach(img=> { observerImg.observe(img) });
</script>
以上代碼就實(shí)現(xiàn)了一個(gè)圖片懶加載功能, 當(dāng)圖片的30%進(jìn)入根元素時(shí)才加載真實(shí)的圖片,這又讓我想起了之前在某條做廣告埋點(diǎn)上報(bào)時(shí)使用react-lazyload的畫面.大家還可以利用它實(shí)現(xiàn)無(wú)限滾動(dòng), H5視差動(dòng)畫等有意思的交互場(chǎng)景.
Mutation Observer主要用來(lái)實(shí)現(xiàn)dom變動(dòng)時(shí)的監(jiān)聽(tīng),同樣也是異步觸發(fā),對(duì)監(jiān)聽(tīng)性能非常友好. Resize Observer主要用來(lái)監(jiān)聽(tīng)元素大小的變化,相比于每次窗口變動(dòng)都觸發(fā)的window.resize事件, Resize Observer有更好的性能和對(duì)dom有更細(xì)粒度的控制,它只會(huì)在繪制前或布局后觸發(fā)調(diào)用. 以上兩個(gè)api的使用和Intersection使用非常類似,官方資料也寫得很全,大家可以好好研究一下.
這個(gè)問(wèn)題主要是之前有朋友問(wèn)過(guò)我,當(dāng)時(shí)的想法就是簡(jiǎn)單的認(rèn)為script內(nèi)的代碼執(zhí)行完之后以及與dom綁定了,存放在了瀏覽器內(nèi)存中,最近查了很多資料發(fā)現(xiàn)有一個(gè)有點(diǎn)意思的解釋,放出來(lái)大家可以感受一下:
JavaScript解釋器在執(zhí)行腳本時(shí),是按塊來(lái)執(zhí)行的,也就是說(shuō)瀏覽器在解析HTML文檔流時(shí),如果遇到一個(gè)script標(biāo)簽,javascript解釋器會(huì)等待這個(gè)代碼塊都加載完了,才進(jìn)行預(yù)編譯,然后才執(zhí)行。所以,當(dāng)開(kāi)始執(zhí)行這個(gè)代碼塊的代碼時(shí),這個(gè)代碼段已經(jīng)被解析完了。這時(shí)再?gòu)腄OM中刪去也就不影響代碼的執(zhí)行了。
Proxy/Reflect雖然是es6的api,出現(xiàn)也已經(jīng)有幾年了,但是在項(xiàng)目中用的還是比較少,如果是做底層架構(gòu)方面的工作,還是建議大家多去使用,畢竟vue/react這種框架源碼把這些api玩的如火純青,還是很有必要掌握一下的。
其實(shí)我們認(rèn)真看mdn的介紹或者阮一峰老師的文章,還是很好理解這些api的用法的,接下來(lái)我們?cè)敿?xì)介紹一下這兩個(gè)api以及應(yīng)用場(chǎng)景.
Proxy 可以理解成,在目標(biāo)對(duì)象之前架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問(wèn),都必須先通過(guò)這層攔截,因此提供了一種機(jī)制,可以對(duì)外界的訪問(wèn)進(jìn)行過(guò)濾和改寫。Proxy在很多場(chǎng)景中都會(huì)和Reflect一起使用. 用法也很簡(jiǎn)單,我們看看Proxy的基本用法:
const obj={
name: '徐小夕',
age: '120'
}
const proxy=new Proxy(obj, {
get(target, propKey, receiver) {
console.log('get:' + propKey)
return Reflect.get(target, propKey, receiver)
},
set(target, propKey, value, receiver) {
console.log('set:' + propKey)
return Reflect.set(target, propKey, value, receiver)
}
})
console.log(proxy.name) // get:name 徐小夕
proxy.work='frontend' // set:work frontend
以上代碼攔截了obj對(duì)象,并重新定義了讀寫(get/set)方法,這樣我們就可以在訪問(wèn)對(duì)象時(shí)進(jìn)行額外的操作了.
Proxy還有apply(攔截 Proxy 實(shí)例作為函數(shù)調(diào)用的操作)和construct(攔截 Proxy 實(shí)例作為構(gòu)造函數(shù)調(diào)用的操作)等屬性可以使用,我們可以在對(duì)象操作的不同階段進(jìn)行攔截,這里我就不一一樣舉例了.接下來(lái)看看Proxy的實(shí)際應(yīng)用場(chǎng)景. * 實(shí)現(xiàn)數(shù)組讀取負(fù)數(shù)的索引
我們一般操作數(shù)組大多數(shù)都是正向操作的,不能通過(guò)指定負(fù)數(shù)來(lái)逆向查找數(shù)組,如下圖:
我們不能通過(guò)arr[-1]來(lái)拿到數(shù)組的尾部元素(字符串同理),這個(gè)時(shí)候我們就可以用Proxy來(lái)實(shí)現(xiàn)這一功能,這是我們的結(jié)構(gòu)有點(diǎn)像環(huán)狀:
這種實(shí)現(xiàn)的好處是如果我們想訪問(wèn)數(shù)組的最后一個(gè)元素時(shí),我們不需要先拿到長(zhǎng)度,再通過(guò)索引訪問(wèn)了:
// 原始寫法
arr[arr.length -1]
// 通過(guò)proxy改造后寫法
arr[-1]
實(shí)現(xiàn)代碼如下:
function createArray(...elements) {
let handler={
get(target, propKey, receiver) {
let index=Number(propKey);
if (index < 0) {
propKey=String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target=[];
target.push(...elements);
return new Proxy(target, handler);
}
我們可以發(fā)現(xiàn)以上代碼使用proxy來(lái)代理數(shù)組的讀取操作,在內(nèi)部封裝了支持負(fù)值查找的功能,當(dāng)然我們也可以不用proxy來(lái)實(shí)現(xiàn)同樣的功能,這里實(shí)現(xiàn)參考阮一峰老師的實(shí)現(xiàn). * 利用proxy實(shí)現(xiàn)更優(yōu)雅的校驗(yàn)器
一般我們?cè)谧霰韱涡r?yàn)的時(shí)候會(huì)寫一些if else或者switch判斷來(lái)實(shí)現(xiàn)對(duì)不同屬性值的校驗(yàn),同樣我們也可以用proxy來(lái)優(yōu)雅的實(shí)現(xiàn)它,代碼如下:
const formData={
name: 'xuxi',
age: 120,
label: ['react', 'vue', 'node', 'javascript']
}
// 校驗(yàn)器
const validators={
name(v) {
// 檢驗(yàn)name是否為字符串并且長(zhǎng)度是否大于3
return typeof v==='string' && v.length > 3
},
age(v) {
// 檢驗(yàn)age是否為數(shù)值
return typeof v==='number'
},
label(v) {
// 檢驗(yàn)label是否為數(shù)組并且長(zhǎng)度是否大于0
return Array.isArray(v) && v.length > 0
}
}
// 代理校驗(yàn)對(duì)象
function proxyValidator(target, validator) {
return new Proxy(target, {
set(target, propKey, value, receiver) {
if(target.hasOwnProperty(propKey)) {
let valid=validator[propKey]
if(!!valid(value)) {
return Reflect.set(target, propKey, value, receiver)
}else {
// 一些其他錯(cuò)誤業(yè)務(wù)...
throw Error(`值驗(yàn)證錯(cuò)誤${propKey}:${value}`)
}
}
}
})
}
有了以上實(shí)現(xiàn)模式,我們就可以實(shí)現(xiàn)對(duì)表單中某個(gè)值進(jìn)行設(shè)置時(shí)進(jìn)行校驗(yàn)了,用法如下:
let formObj=proxyValidator(formData, validators)
formObj.name=333; // Uncaught Error: 值驗(yàn)證錯(cuò)誤name:f
formObj.age='ddd' // Uncaught Error: 值驗(yàn)證錯(cuò)誤age:f
以上代碼中當(dāng)設(shè)置了不合法的值時(shí),控制臺(tái)將會(huì)剖出錯(cuò)誤,如果在實(shí)際業(yè)務(wù)中,我們可以給用戶做出適當(dāng)?shù)奶嵝? 實(shí)現(xiàn)請(qǐng)求攔截和錯(cuò)誤上報(bào) 實(shí)現(xiàn)數(shù)據(jù)過(guò)濾
以上幾點(diǎn)筆者在之前的文章中也寫過(guò),所以這里不在詳細(xì)介紹了.大家也可以根據(jù)實(shí)際情況自己實(shí)現(xiàn)更加靈活的攔截操作.當(dāng)然Proxy提供的API遠(yuǎn)遠(yuǎn)不止這幾個(gè),我們可以在MDN或者其他渠道了解更多高級(jí)用法.
Reflect對(duì)象與Proxy對(duì)象一樣,也是 ES6 為了操作對(duì)象而提供的新 API,更多的應(yīng)用場(chǎng)景是配合proxy一起使用,在上文中已經(jīng)用到了.可以將Object對(duì)象的一些明顯屬于語(yǔ)言內(nèi)部的方法放到Reflect對(duì)象上,并修改某些Object方法的返回結(jié)果. Reflect對(duì)象的方法與Proxy對(duì)象的方法一一對(duì)應(yīng),只要是Proxy對(duì)象的方法,就能在Reflect對(duì)象上找到對(duì)應(yīng)的方法。
CustomEvent API是個(gè)非常有意思的api, 而且非常實(shí)用, 更重要的是學(xué)起來(lái)非常簡(jiǎn)單,而且被大部分現(xiàn)代瀏覽器支持.我們可以讓任意dom元素監(jiān)聽(tīng)和觸發(fā)自定義事件,只需要如下操作:
// 添加一個(gè)適當(dāng)?shù)氖录O(jiān)聽(tīng)器
dom1.addEventListener("boom", function(e) { something(e.detail.num) })
// 創(chuàng)建并分發(fā)事件
var event=new CustomEvent("boom", {"detail":{"num":10}})
dom1.dispatchEvent(event)
我們來(lái)看看CustomEvent的參數(shù)介紹: type 事件的類型名稱,如上面代碼中的'boom' CustomEventInit 提供了事件的配置信息,具體有以下幾個(gè)屬性 * bubbles 一個(gè)布爾值,表明該事件是否會(huì)冒泡 * cancelable 一個(gè)布爾值,表明該事件是否可以被取消 * detail 當(dāng)事件初始化時(shí)傳遞的數(shù)據(jù)
我們可以通過(guò)dispatchEvent來(lái)觸發(fā)自定義事件.其實(shí)他的用途有很多,比如創(chuàng)建觀察者模式, 實(shí)現(xiàn)數(shù)據(jù)雙向綁定, 亦或者在游戲開(kāi)發(fā)中實(shí)現(xiàn)打怪掉血,比如下面的例子:
筆者上面畫了一個(gè)打boss的草圖, 現(xiàn)在的場(chǎng)景是兩個(gè)玩家一起打boss, 我們可以在玩家發(fā)動(dòng)攻擊的時(shí)候觸發(fā)dispatch掉血的自定義事件, boss監(jiān)聽(tīng)到事件后將血量自動(dòng)扣除, 至于不同角色的傷害值,我們可以存放在detail中,然后通過(guò)策略模式去分發(fā)傷害.筆者曾今在學(xué)校開(kāi)發(fā)的H5游戲時(shí)就大量采用類似的模式,還是非常有意思的.
File API使得我們?cè)跒g覽器端可以訪問(wèn)文件的數(shù)據(jù),比如預(yù)覽文件,獲取文件信息(比如文件名,文件內(nèi)容,文件大小等), 并且可以在前端實(shí)現(xiàn)文件下載(可以借助canvas和 window.URL.revokeObjectURL的一些能力).當(dāng)然我們還可以實(shí)現(xiàn)拖拽上傳文件這樣高用戶體驗(yàn)的操作.接下來(lái)我們來(lái)看看幾個(gè)實(shí)際例子. * 顯示縮略圖
function previewFiles(files, previewBox) {
for (var i=0; i < files.length; i++) {
var file=files[i];
var imageType=/^image\//;
if (!imageType.test(file.type)) {
continue;
}
var img=document.createElement("img");
previewBox.appendChild(img); // 假設(shè)"preview"就是用來(lái)顯示內(nèi)容的div
var reader=new FileReader();
reader.onload=(function(imgEl) {
return function(e) { imgEl.src=e.target.result; };
})(img);
reader.readAsDataURL(file);
}
}
以上代碼可以在reviewBox容器中顯示已上傳好的圖片,當(dāng)然我們還可以基于此來(lái)擴(kuò)展,利用canvas將圖片畫到canvas上,然后進(jìn)行圖片壓縮,最后再把壓縮后的圖片上傳到服務(wù)器.這中方式其實(shí)目前很多工具型網(wǎng)站都在用,比如在線圖片處理網(wǎng)站,提供的批量壓縮圖片,批處理水印等功能,套路都差不多,感興趣的朋友可以嘗試研究一下. * 封裝文件上傳組件
這塊筆者之前也寫過(guò)詳細(xì)的文章,這里就不一一舉例了.
6. Fullscreen
全屏API主要是讓網(wǎng)頁(yè)能在電腦屏幕中全屏顯示,它允許我們打開(kāi)或者退出全屏模式,以便我們根據(jù)需要進(jìn)行對(duì)應(yīng)的操作,比如我們常用的網(wǎng)頁(yè)圖形編輯器或者富文本編輯器, 為了讓用戶專心于內(nèi)容設(shè)計(jì),我們往往提供切換全屏的功能供用戶使用.由于全屏API比較簡(jiǎn)單,這里我們直接上代碼:
// 開(kāi)啟全屏
document.documentElement.requestFullscreen();
// 退出全屏
document.exitFullscreen();
以上代碼的document.documentElement也可以換成任何一個(gè)你想讓其全屏的元素.默認(rèn)情況下我們還可以通過(guò)document.fullscreenElement來(lái)判斷當(dāng)前頁(yè)面是否處于全屏狀態(tài),來(lái)實(shí)現(xiàn)屏幕切換的效果.如果是react開(kāi)發(fā)者,我們也可以將其封裝成一個(gè)自定義hooks來(lái)實(shí)現(xiàn)與業(yè)務(wù)相關(guān)的全屏切換功能.
URL API是URL標(biāo)準(zhǔn)的組成部分,URL標(biāo)準(zhǔn)定義了構(gòu)成有效統(tǒng)一資源定位符的內(nèi)容以及訪問(wèn)和操作URL的API。
我們利用URL組件可以做很多有意思的事情.比如我們有個(gè)需求需要提取url的參數(shù)傳給后臺(tái),傳統(tǒng)的做法是自己寫一個(gè)方法來(lái)解析url字符串,手動(dòng)返回一個(gè)query對(duì)象.但是利用URL對(duì)象,我們可以很方便的拿到url參數(shù),如下:
let addr=new URL(window.location.href)
let host=addr.host // 獲取主機(jī)地址
let path=addr.pathname // 獲取路徑名
let user=addr.searchParams.get("user") // 獲取參數(shù)為user對(duì)應(yīng)的值
以上代碼可知,我們?nèi)绻麑rl轉(zhuǎn)化為URL對(duì)象,那么我們就可以很方便的通過(guò)searchParams提供的api來(lái)拿到url參數(shù)而無(wú)需自己再寫一個(gè)方法了.
另一方面,如果網(wǎng)站安全性比較高,我們還可以對(duì)參數(shù)進(jìn)行自然數(shù)排序然后再加密上傳給后端.具體代碼如下:
function sortMD5WithParameters() {
let url=new URL(document.location.href);
url.searchParams.sort();
let keys=url.searchParams.keys();
let params={}
for (let key of keys) {
let val=url.searchParams.get(key);
params[key]=val
};
// ...md5加密
return MD5(params)
}
地理位置 API 通過(guò) navigator.geolocation 提供, 這個(gè)瀏覽器API也比較實(shí)用, 我們?cè)诰W(wǎng)站中可以用此方式確定用戶的位置信息,從而讓網(wǎng)站有不同的展現(xiàn),增強(qiáng)用戶體驗(yàn).
舉幾個(gè)有意思的例子可以讓大家感受一下: 根據(jù)不同地區(qū),網(wǎng)站展示不同的主題:
根據(jù)用戶所在地區(qū),展示不同推薦內(nèi)容 這一點(diǎn)電商網(wǎng)站或者內(nèi)容網(wǎng)站用的比較多, 比如用戶在新疆,則給他推薦瓜果類廣告, 在北京,則給他推薦旅游景點(diǎn)類廣告等,雖然實(shí)際應(yīng)用中往往會(huì)更復(fù)雜,但是也是一種思路.
其實(shí)應(yīng)用遠(yuǎn)遠(yuǎn)不止如此,程序員可以發(fā)揮想象來(lái)實(shí)現(xiàn)更有意思的事情,讓自己的網(wǎng)站更智能.接下來(lái)筆者就基于promise寫一段獲取用戶位置的代碼:
function getUserLocation() {
return new Promise((resolve, reject)=> {
if (!navigator.geolocation) {
reject()
} else {
navigator.geolocation.getCurrentPosition(success, error);
}
function success(position) {
const latitude=position.coords.latitude;
const longitude=position.coords.longitude;
resolve({latitude, longitude})
}
function error() {
reject()
}
})
}
使用方式和結(jié)果如下圖所示:
我們基于獲取到的經(jīng)緯度調(diào)用第三方api(比如百度,高德)就可以獲取用戶所在為精確位置信息了.
Notifications API 允許網(wǎng)頁(yè)或應(yīng)用程序在系統(tǒng)級(jí)別發(fā)送在頁(yè)面外部顯示的通知;這樣即使應(yīng)用程序空閑或在后臺(tái),Web應(yīng)用程序也會(huì)向用戶發(fā)送信息。
我們舉個(gè)實(shí)際的例子,比如我們網(wǎng)站內(nèi)容有更新,通知用戶,效果如下:
相關(guān)代碼如下:
Notification.requestPermission( function(status) {
console.log(status); // 僅當(dāng)值為 "granted" 時(shí)顯示通知
var n=new Notification("趣談前端", {body: "從零搭建一個(gè)CMS全棧項(xiàng)目"}); // 顯示通知
});
當(dāng)然瀏覽器的Notification還給我們提供了4個(gè)事件觸發(fā)api方便我們做更全面的控制: onshow 當(dāng)通知被顯示給用戶時(shí)觸發(fā) (已廢棄, 但部分瀏覽器仍然能用) onclick 當(dāng)用戶點(diǎn)擊通知時(shí)觸發(fā) onclose 當(dāng)通知被關(guān)閉時(shí)觸發(fā)(已廢棄, 但部分瀏覽器仍然能用) onerror 當(dāng)通知發(fā)生錯(cuò)誤的時(shí)候觸發(fā)
有了這樣的事件監(jiān)聽(tīng),我們就可以控制當(dāng)用戶點(diǎn)擊通知時(shí), 跳轉(zhuǎn)到對(duì)應(yīng)的頁(yè)面或者執(zhí)行相關(guān)的業(yè)務(wù)邏輯.如下代碼所示:
Notification.requestPermission( function(status) {
console.log(status); // 僅當(dāng)值為 "granted" 時(shí)顯示通知
var n=new Notification("趣談前端", {body: "從零搭建一個(gè)CMS全棧項(xiàng)目"}); // 顯示通知
n.onshow=function () {
// 消息顯示時(shí)執(zhí)行的邏輯
console.log('show')
}
n.onclick=function () {
// 消息被點(diǎn)擊時(shí)執(zhí)行的邏輯
history.push('/detail/1232432')
}
n.onclose=function () {
// 消息關(guān)閉時(shí)執(zhí)行的邏輯
console.log('close')
}
});
當(dāng)然我們?cè)谑褂们靶枰@取權(quán)限,方式也很簡(jiǎn)單,大家可以在mdn上學(xué)習(xí)了解.
Battery Status API提供了有關(guān)系統(tǒng)充電級(jí)別的信息并提供了通過(guò)電池等級(jí)或者充電狀態(tài)的改變提醒用戶的事件。 這個(gè)可以在設(shè)備電量低的時(shí)候調(diào)整應(yīng)用的資源使用狀態(tài),或者在電池用盡前保存應(yīng)用中的修改以防數(shù)據(jù)丟失。
之前的版本中Battery Status API提供了幾個(gè)事件監(jiān)聽(tīng)函數(shù)來(lái)監(jiān)聽(tīng)電量的變化以及監(jiān)聽(tīng)設(shè)備是否充電,但是筆者看文檔時(shí)這些api都已經(jīng)廢棄,如下: chargingchange 監(jiān)聽(tīng)設(shè)別是否充電 levelchange 監(jiān)聽(tīng)電量充電等級(jí) chargingtimechange 充電時(shí)間變化 dischargingtimechange 放電時(shí)間變化
雖然以上幾個(gè)看似有用的api已經(jīng)被棄用,但是筆者親測(cè)谷歌還是可以正常使用的,但是為了讓自己代碼更可靠,我們可以用其他方式代替,比如用定時(shí)器定期去檢測(cè)電量情況,進(jìn)而對(duì)用戶做出不同的提醒.
接下來(lái)我們看看基本的用法:
navigator.getBattery().then(function(battery) {
console.log("是否在充電? " + (battery.charging ? "是" : "否"));
console.log("電量等級(jí): " + battery.level * 100 + "%");
console.log("充電時(shí)間: " + battery.chargingTime + " s");
console.log("放電時(shí)間: " + battery.dischargingTime + "s");
});
我們可以通過(guò)getBattery拿到設(shè)備電池信息,這個(gè)api非常有用,比如我們可以在用戶電量不足時(shí)禁用網(wǎng)站動(dòng)畫或者停用一些耗時(shí)任務(wù),亦或者是對(duì)用戶做適當(dāng)?shù)奶嵝?改變網(wǎng)站顏色等,對(duì)于webapp中播放視頻或者直播時(shí),我們也可以用css畫一個(gè)電量條,當(dāng)電量告急時(shí)提醒用戶.作為一個(gè)優(yōu)秀的網(wǎng)站體驗(yàn)師,這一塊還是不容忽視的.
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。