创建自定义天空
高清渲染管线 (High Definition Render Pipeline, HDRP) 使用的天空系统允许您使用该系统自身的属性和着色器开发您自己的自定义天空,同时仍保持天空与光照管线一致。
要创建您自己的天空,请创建一些脚本来处理以下内容:
使用天空渲染器
完成上述所有步骤后,新天空将自动显示在 Unity 项目 Volumes 的 Visual Environment 覆盖中的 Sky Type 下拉选单下。
天空设置
首先,创建一个继承自 SkySettings 的新类。此新类包含所需的特定天空渲染器特有的所有属性。
此类必须包括以下内容:
- SkyUniqueID 属性:此属性必须是该特定天空特有的唯一整数;不得与任何其他 SkySettings 冲突。使用 SkyType 枚举可以查看 HDRP 已经使用的值。
- GetHashCode:天空系统使用此函数确定何时重新渲染天空反射立方体贴图。
- GetSkyRendererType:天空系统使用此函数来实例化正确的渲染器。
例如,以下是 SkySettings 的 HDRI Sky 实现:
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
[VolumeComponentMenu("Sky/New Sky")]
// SkyUniqueID 不必属于内置 HDRP SkyType 枚举。
// 只是为了跟踪 HDRP 本机使用的 ID 才提供。
// 可以使用任何整数值。
[SkyUniqueID(NEW_SKY_UNIQUE_ID)]
public class NewSky : SkySettings
{
const int NEW_SKY_UNIQUE_ID = 20382390;
[Tooltip("Specify the cubemap HDRP uses to render the sky.")]
public CubemapParameter hdriSky = new CubemapParameter(null);
public override Type GetSkyRendererType()
{
return typeof(NewSkyRenderer);
}
public override int GetHashCode()
{
int hash = base.GetHashCode();
unchecked
{
hash = hdriSky.value != null ? hash * 23 + hdriSky.GetHashCode() : hash;
}
return hash;
}
public override int GetHashCode(Camera camera)
{
// 天空取决于摄像机设置(例如位置)时实现
return GetHashCode();
}
}
天空渲染器
现在,必须创建实际渲染天空(无论是渲染为光照立方体贴图还是背景可视化渲染)的类。在此处必须实现具体渲染功能。
您的 SkyRenderer 必须实现 SkyRenderer 接口:
public abstract class SkyRenderer
{
int m_LastFrameUpdate = -1;
/// <summary>
/// 启动时调用。创建渲染器使用的资源(着色器、材质等)。
/// </summary>
public abstract void Build();
/// <summary>
/// 清理时调用。发布渲染器使用的资源。
/// </summary>
public abstract void Cleanup();
/// <summary>
/// HDRP 每帧调用一次此函数。如果您的 SkyRenderer 需要独立于用户定义的更新频率进行迭代,则实现此函数(请参阅 SkySettings UpdateMode)。
/// </summary>
/// <returns>如果通过更新确定了需要重新渲染天空光照,则返回 true。否则返回 false。</returns>
protected virtual bool Update(BuiltinSkyParameters builtinParams) { return false; }
/// <summary>
/// 实现天空的实际渲染。在将天空渲染到立方体贴图(用于光照)以及在主帧渲染期间,HDRP 都会调用此方法。
/// </summary>
/// <param name="builtinParams">可用于渲染天空的引擎参数。</param>
/// <param name="renderForCubemap">如果要将天空渲染到立方体贴图以用于光照,传入 true。在这种情况下,天空渲染器需要其他实现,因此很有用。</param>
/// <param name="renderSunDisk">如果天空渲染器支持渲染太阳圆盘,在此参数设置为 false 时,不会渲染太阳圆盘。</param>
public abstract void RenderSky(BuiltinSkyParameters builtinParams, bool renderForCubemap, bool renderSunDisk);
例如,以下是 SkyRenderer 的简单实现:
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
class NewSkyRenderer : SkyRenderer
{
public static readonly int _Cubemap = Shader.PropertyToID("_Cubemap");
public static readonly int _SkyParam = Shader.PropertyToID("_SkyParam");
public static readonly int _PixelCoordToViewDirWS = Shader.PropertyToID("_PixelCoordToViewDirWS");
Material m_NewSkyMaterial; // 将立方体贴图渲染为渲染纹理(可以是立方体或 2D)
MaterialPropertyBlock m_PropertyBlock = new MaterialPropertyBlock();
private static int m_RenderCubemapID = 0; // FragBaking
private static int m_RenderFullscreenSkyID = 1; // FragRender
public override void Build()
{
m_NewSkyMaterial = CoreUtils.CreateEngineMaterial(GetNewSkyShader());
}
// 采用依赖于项目的方式来获取着色器。
Shader GetNewSkyShader()
{
// 实现我
return null;
}
public override void Cleanup()
{
CoreUtils.Destroy(m_NewSkyMaterial);
}
protected override bool Update(BuiltinSkyParameters builtinParams)
{
return false;
}
public override void RenderSky(BuiltinSkyParameters builtinParams, bool renderForCubemap, bool renderSunDisk)
{
using (new ProfilingSample(builtinParams.commandBuffer, "Draw sky"))
{
var newSky = builtinParams.skySettings as NewSky;
int passID = renderForCubemap ? m_RenderCubemapID : m_RenderFullscreenSkyID;
float intensity = GetSkyIntensity(newSky, builtinParams.debugSettings);
float phi = -Mathf.Deg2Rad * newSky.rotation.value; // -此旋转是为了匹配旧版
m_PropertyBlock.SetTexture(_Cubemap, newSky.hdriSky.value);
m_PropertyBlock.SetVector(_SkyParam, new Vector4(intensity, 0.0f, Mathf.Cos(phi), Mathf.Sin(phi)));
m_PropertyBlock.SetMatrix(_PixelCoordToViewDirWS, builtinParams.pixelCoordToViewDirMatrix);
CoreUtils.DrawFullScreen(builtinParams.commandBuffer, m_NewSkyMaterial, m_PropertyBlock, passID);
}
}
}
重要注意事项:
如果你的天空渲染器必须管理大量数据(例如预先计算的纹理或类似内容),则必须格外小心。实际上,每个摄像机都会存在一个渲染器实例,因此默认情况下,如果此数据是渲染器的一部分,此数据也将复制到内存中。 由于每个天空渲染器可能有完全不同的需求,因此由渲染器负责共享此类数据,并且需要由用户实现。
天空渲染着色器
最后,您需要为天空实际创建着色器。此着色器的内容取决于希望包含的效果。
例如,以下是 SkyRenderer 的 HDRI Sky 实现。
Shader "Hidden/HDRP/Sky/NewSky"
{
HLSLINCLUDE
#pragma vertex Vert
#pragma editor_sync_compilation
#pragma target 4.5
#pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonLighting.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Sky/SkyUtils.hlsl"
TEXTURECUBE(_Cubemap);
SAMPLER(sampler_Cubemap);
float4 _SkyParam; // x 曝光,y 乘数,zw 旋转(cosPhi 和 sinPhi)
#define _Intensity _SkyParam.x
#define _CosPhi _SkyParam.z
#define _SinPhi _SkyParam.w
#define _CosSinPhi _SkyParam.zw
struct Attributes
{
uint vertexID : SV_VertexID;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
UNITY_VERTEX_OUTPUT_STEREO
};
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID, UNITY_RAW_FAR_CLIP_VALUE);
return output;
}
float3 RotationUp(float3 p, float2 cos_sin)
{
float3 rotDirX = float3(cos_sin.x, 0, -cos_sin.y);
float3 rotDirY = float3(cos_sin.y, 0, cos_sin.x);
return float3(dot(rotDirX, p), p.y, dot(rotDirY, p));
}
float4 GetColorWithRotation(float3 dir, float exposure, float2 cos_sin)
{
dir = RotationUp(dir, cos_sin);
float3 skyColor = SAMPLE_TEXTURECUBE_LOD(_Cubemap, sampler_Cubemap, dir, 0).rgb * _Intensity * exposure;
skyColor = ClampToFloat16Max(skyColor);
return float4(skyColor, 1.0);
}
float4 RenderSky(Varyings input, float exposure)
{
float3 viewDirWS = GetSkyViewDirWS(input.positionCS.xy);
// 反转方向以指向场景
float3 dir = -viewDirWS;
return GetColorWithRotation(dir, exposure, _CosSinPhi);
}
float4 FragBaking(Varyings input) : SV_Target
{
return RenderSky(input, 1.0);
}
float4 FragRender(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
return RenderSky(input, GetCurrentExposureMultiplier());
}
ENDHLSL
SubShader
{
// 常规新天空
// 对于立方体贴图
Pass
{
ZWrite Off
ZTest Always
Blend Off
Cull Off
HLSLPROGRAM
#pragma fragment FragBaking
ENDHLSL
}
// 对于全屏天空
Pass
{
ZWrite Off
ZTest LEqual
Blend Off
Cull Off
HLSLPROGRAM
#pragma fragment FragRender
ENDHLSL
}
}
Fallback Off
}
注意:NewSky 示例使用两个通道:一个使用深度测试在背景中渲染天空(以便几何体将其正确遮挡),另一个不使用深度测试并会将天空渲染为反射立方体贴图。