2023-06-29 11:55:02 +08:00

21 KiB
Raw Blame History

实用库

#include "/Engine/Private/Random.ush" #include "/Engine/Private/SobolRandom.ush"

眼睛

眼睛焦散:

凹模型

直接贴图即可。 折射 模型高光

凸模型

  1. 使用视差贴图来控制瞳孔效果 + 折射模拟
float2 viewL = mul(viewW, (float3x2) worldInverse);
float2 offset = height * viewL;
offset.y = -offset.y;
texcoord -= parallaxScale * offset;

  1. Physically based refraction
// 角膜区域突起的模型
// Alternatively, use a displacement map
// height = max(-positionL.z  eyeIrisDepth, 0.0); 

// 球形模型
// Plot[Max[1.0 - 18.4 * r * r, 0.0], {r, 0, 0.3}]
height = anteriorChamberDepth * saturate( 1.0 - 18.4 * radius * radius ); 

// refractedW
float w = n * dot( normalW, viewW );
float k = sqrt( 1.0 + ( w - n ) * ( w + n ) );
float3 refractedW = ( w - k ) * normalW - n * viewW;

float cosAlpha = dot(frontNormalW, -refractedW);
float dist = height / cosAlpha;
float3 offsetW = dist * refractedW;
float2 offsetL = mul(offsetW, (float3x2) worldInverse);
texcoord += float2(mask, -mask) * offsetL;

代码中首先计算了height即前房的高度PPT中height有两种计算方式分别对应两种眼睛的模型结构对应的结构写在注释中了。 然后计算了refracted这个是rtr中快速拟合的计算方法n是空气与介质折射率的比值关于refracted的推论可以参考 YivanLee虚幻4渲染编程(人物篇)【第三卷Human Eye Rendering】 129 赞同 · 12 评论文章

最后一段先通过frontNormalW与refractedW的点积计算出α角的cos值上图中的α应该是标识错误α是-refractedW与frontNormalW的夹角。然后已知height通过比值可以计算出refractedW的模长dist。offsetW即为完整的refractedW向量。最后转换到本地空间乘上眼睛的Mask加到原本的UV上。 之后就是使用偏转后的UV去采样贴图了。 与视差相同这里也是在本地与世界空间中进行的计算同样会有轴向问题主要是normalW、viewW和frontNormalW参与的计算normal与view可以转换到切线空间计算而frontNormalW代表的是模型向前的朝向这个必须要指定不过图方便的话把frontNormalW改成切线空间法线也不是不可以。

多层复合模型

樱花大战cedec2020分享https://blog.ch-wind.com/cedec2020-new-sakura-wars-note/

本作的眼睛分为三个部分,眼白的部分是一个内凹的形状,瞳孔的部分则分为了向内凹的部分和向外突出的部分。

瞳孔的高光叠加在其突出的半透明部分上。根据摄像角度的不同,各个部分的贴图会分开进行移动,使得在哪个角度高光都能处在一个刚好的位置。 控制上,有针对高光上下左右的移动强度与控制移动范围的参数共同作用。

从左边开始是作为基础颜色的Albedo以及用于Mask瞳孔的Alpha贴图用于在Albedo上进行叠加的spt贴图以及两张瞳孔高光以及反应环境的matcapture贴图。 虽然很多动画风格的渲染中会省略掉瞳孔中的虹彩部分但是本作为了提高角色靠近时的效果进行了详细的绘制同时为了体现环境的变化与matcap的贴图进行叠加。 高光贴图有两张分别使用不同的UV动画进行控制用于表现眼睛的湿润感。虽然是很细微的操作但是对于表现角色的感情非常的有用。

SunnySideUp UnityChan

其他效果实现

眼睛高光效果

  1. 贴图高光。使用事先绘制的高光形状贴图贴到最外面的。并且使用ViewDirection来控制。设定4个UV Coord, 根据 View=》眼睛的本地坐标系=》Normalize后的向量进行插值。
  2. PBR的思路

Matcap反射效果

Matcap材质+球形法线贴图

float3 NormalBlend_MatcapUV_Detail = viewNormal.rgb * float3(-1,-1,1);
float3 NormalBlend_MatcapUV_Base = (mul( UNITY_MATRIX_V, float4(viewDirection,0)).rgb*float3(-1,-1,1)) + float3(0,0,1);
float3 noSknewViewNormal = NormalBlend_MatcapUV_Base * dot(NormalBlend_MatcapUV_Base, NormalBlend_MatcapUV_Detail) / NormalBlend_MatcapUV_Base.b - NormalBlend_MatcapUV_Detail;                
float2 ViewNormalAsMatCapUV = noSknewViewNormal.rg * 0.5 + 0.5;

焦散效果

焦散的表现反倒简单了,直接画在眼睛贴图上都可以,考虑到卡通表达的自由性,焦散是否存在与焦散的形状都可以没有限制,只要好看就行。 下图也是miHoYo的分享可以简单的理解为直接贴张Mask上去然后用光照方向和菲涅尔去影响强度变化。

使用Mask贴图、NoL与菲尼尔来控制

其他游戏方案

大致方案

  • 主要分为在材质编辑器中完成大部分渲染
  • 传递少量参数到Lighting阶段并进行光照计算
  • 以上两者结合

待实现功能

  • 顶点色控制Outline宽度使用顶点色G。
  • 使用罪恶装备中的lightmap 控制高光的区域和阈值,包括顶点色可以精准控制高光的强度。

具体实现

全局设置实现

AWorldSettings存储于ULevel中。可以通过Project Settings->Engine->General Settings->Default Classes来修改成其他的类作为默认类。

 [/Script/Engine.Engine]
 WorldSettingsClassName=/Script/MySpiffyGame.MySpiffyGameWorldSettings

C++中修改Settings类void ULevel::SetWorldSettings(AWorldSettings* NewWorldSettings)或许可以考虑子系统。

Shader与管线变量添加

shadowAttenuation = mainLight.shadowAttenuation;

float _SystemShadowsLevel_var = (shadowAttenuation*0.5)+0.5+_Tweak_SystemShadowsLevel > 0.001 ? (shadowAttenuation*0.5)+0.5+_Tweak_SystemShadowsLevel : 0.0001;
float Set_FinalShadowMask = saturate((1.0 + ( (lerp( _HalfLambert_var, _HalfLambert_var*saturate(_SystemShadowsLevel_var), _Set_SystemShadowsToBase ) - (_BaseColor_Step-_BaseShade_Feather)) * ((1.0 - _Set_1st_ShadePosition_var.rgb).r - 1.0) ) / (_BaseColor_Step - (_BaseColor_Step-_BaseShade_Feather))));

Ramp相关变量

  • shadowAttenuation 默认为1方向光没有这个参数与 Tweak_SystemShadowsLevel先暂时合并为HalfLambertFix
  • Step
  • Feather

Shader开关变量

  • Is_LightColor_BaseColorBaseColor是否受到LightColor影响
  • Is_LightColor_ShadeColor1st_ShadeColor是否受到LightColor影响

FViewUniformShaderParameter添加数据

顶点色

用于存储一些低精度数据,插值即可

  • R:
  • G:描边宽度
  • B:

蓝色协议的R:阴影区域标记 与 B:Ao而罪恶装备使用贴图来传递信息。

lightmap

,G为阴影控AOR为高光强度参数金属和光滑材质的部分设置的更大一些。B通道用于照明控制。最大值为高光反之值越小高光越淡。

https://zhuanlan.zhihu.com/p/360229590一文中介绍了崩坏3与原神的计算方式

崩坏3的LightMap计算方式

half4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv.xy);
half4 LightMapColor = SAMPLE_TEXTURE2D(_LightMap, sampler_LightMap, input.uv.xy);
half3 ShadowColor = baseColor.rgb * _ShadowMultColor.rgb;
half3 DarkShadowColor = baseColor.rgb * _DarkShadowMultColor.rgb;

//如果SFactor = 0,ShallowShadowColor为一级阴影色,否则为BaseColor。
float SWeight = (LightMapColor.g * input.color.r + input.lambert) * 0.5 + 1.125;
float SFactor = floor(SWeight - _ShadowArea);
half3 ShallowShadowColor = SFactor * baseColor.rgb + (1 - SFactor) * ShadowColor.rgb;

二级阴影计算:

//如果SFactor = 0,DarkShadowColor为二级阴影色,否则为一级阴影色。
SFactor = floor(SWeight - _DarkShadowArea);
DarkShadowColor = SFactor * (_FixDarkShadow * ShadowColor + (1 - _FixDarkShadow) * ShallowShadowColor) + (1 - SFactor) * DarkShadowColor;

// 平滑阴影边缘
half rampS = smoothstep(0, _ShadowSmooth, input.lambert - _ShadowArea);
half rampDS = smoothstep(0, _DarkShadowSmooth, input.lambert - _DarkShadowArea);
ShallowShadowColor.rgb = lerp(ShadowColor, baseColor.rgb, rampS);
DarkShadowColor.rgb = lerp(DarkShadowColor.rgb, ShadowColor, rampDS);

//如果SFactor = 0,FinalColor为二级阴影否则为一级阴影。
SFactor = floor(LightMapColor.g * input.color.r + 0.9f);
half4 FinalColor;
FinalColor.rgb = SFactor * ShallowShadowColor + (1 - SFactor) * DarkShadowColor;

罪恶装备 对阴影判断阈值的偏移。见前面着色部分顶点AO+手绘修正) G : 轮廓线根据与相机的距离扩大多少的系数 B : 等高线 Z 轴偏移值 A : 轮廓厚度系数。0.5为标准1为最大厚度0为无等高线

描边

蓝色协议

  1. 使用Sobel过滤器进行深度检测描边。
  2. 使用Sobel过滤器进行Id图检测描边。
  3. 使用Sobel过滤器进行Normal检测描边。用于处理一些难以分ID深度差又很小的地方通过获取周围点法线求点乘的方式判断出轮廓。![[08-Assets/Images/ImageBag/UrealEngineNPR/蓝色协议_Normal检测描边.png)
  4. 预先画好的轮廓GBuffer

所以使用需要 OutlineId、OutlineWidth感觉可以传递一个全局Outline信息贴图再通过ID查表来获取但只能在角色较少时使用、OutlinePaint 、OutlineZShift个人感觉不需要

边缘光

RimLighting

接触阴影

面部阴影

使用Face ShaderModel修改法线。描边使用ObjectPivot 缩放进行扩边。

后处理

FSceneView存储FFinalPostProcessSettings FinalPostProcessSettings场景的后处理信息。 FBloomOutputs AddBloomPass(FRDGBuilder& GraphBuilder, const FViewInfo& View, const FBloomInputs& Inputs)

GBuffer

OutGBufferA = WorldNormal/PerObjectGBufferData
OutGBufferB = Metallic/Specular/Roughness/EncodeShadingModelIdAndSelectiveOutputMask(GBuffer.ShadingModelID, GBuffer.SelectiveOutputMask);
OutGBufferC = BaseColor/GBufferAO
OutGBufferD = GBuffer.CustomData;
OutGBufferE = GBuffer.PrecomputedShadowFactors;
GBufferB:Metallic/Specular/Roughness=>ToonHairMask OffsetShadowMask/SpcularMask/SpecularValue
OutGBufferD = CustomData.xyzw=ShaderColor.rgb/NoL
OutGBufferE = GBuffer.PrecomputedShadowFactors.xyzw= /RimLightMask/DiffuseOffset/RimLightWidth
OutGBufferF = velocity => OutlineWidth/OutlineID/OutlinePaint/OutlineZShift
| GBuffer  | 表头                                                                                  |
| -------- | ------------------------------------------------------------------------------------- |
| GBufferB | OffsetShadowMask SpcularMask SpecularValue EncodeShadingModelIdAndSelectiveOutputMask |
| GBufferD | ShaderColor.rgb NoL                                                                   |
| GBufferE |                                                                                       |
| GBufferF | ID                                                                                    |

BaseColor与ShadowColor

  • 原神里ShadowColor还会接收其他物体的阴影投射没有自投影;蓝色协议可能也没有自投影。

BaseColor与ShadowColor的过渡需要Step、Feather、Offset等参数可以直接制作一个HalfLambert的渐变贴图之后使用View传递。因为有多个贴图所以还需要ID贴图指定。但这样需要考虑一个问题

  • 一个物体上的同一个ID区域的BaseColor与ShadowColor是否都是一样的
  • 如果不一样就需要再传递一个ShadowColor.rgb到GBuffer里。
  • 不管如何手绘的补充暗部也是需要加到GBuffer中的

这决定传递到View里面的渐变贴图是彩色还是暗色

预计算贴图方案(构想)

Toon渲染一般会使用HalfLambda。之后使用Feather、Step等参数对过渡边界进行调整 使用 渐变贴图查表来实现 渐变、二阶化。以此代替羽化、step等参数。 使用ID贴图指定或者通过BaseColor值来查询

高光

  • PBR高光使用Roughness控制是否可行是否需要传入GBuffer一个Mask贴图
  • 自定义高光:高光贴图、高光颜色、参数化高光形状、多层高光

描边

  • 原神的描边好像是后处理
  • 蓝色协议 !08-Assets/Images/ImageBag/UrealEngineNPR/原神_描边.png !08-Assets/Images/ImageBag/UrealEngineNPR/原神截图_描边.png

TODO考虑使用顶点色来控制宽度使用顶点色G

多光源

主方向光提供照明与Shadow,其他光只提供照亮效果。

!08-Assets/Images/ImageBag/UrealEngineNPR/原神截图_光照.png

有关眉毛、表情需要使用 模板功能

UTS使用模板 !08-Assets/Images/ImageBag/UrealEngineNPR/UTS表情.png !08-Assets/Images/ImageBag/UrealEngineNPR/UTS表情_StencilOut.png !08-Assets/Images/ImageBag/UrealEngineNPR/UTS表情_StencilMask.png

Stencil
{
    Ref[_StencilNo]             //设置渲染的模板缓存值0~255
    Comp[_StencilComp]          //模板测试的通过条件有除了equal还有Greater、Less、Always、Never等类似ZTest。
    Pass[_StencilOpPass]        //表示通过模板测试和Z测试注意是都通过的像素怎么处置它的模板值。
    Fail[_StencilOpFail]        //表示通过了模板测试但没通过Z测试的像素怎么处置它的模板值。
}

_UTS_StencilMode mode = (_UTS_StencilMode)(material.GetInt(ShaderPropStencilMode));
switch (mode)
{
    case _UTS_StencilMode.Off:
        //    material.SetInt(ShaderPropStencilNo,0);
        material.SetInt(ShaderPropStencilComp, (int)_StencilCompFunction.Disabled);
        material.SetInt(ShaderPropStencilOpPass, (int)_StencilOperation.Keep);
        material.SetInt(ShaderPropStencilOpFail, (int)_StencilOperation.Keep);
        break;
    case _UTS_StencilMode.StencilMask:
        //    material.SetInt(ShaderPropStencilNo,0);
        material.SetInt(ShaderPropStencilComp, (int)_StencilCompFunction.Always);
        material.SetInt(ShaderPropStencilOpPass, (int)_StencilOperation.Replace);
        material.SetInt(ShaderPropStencilOpFail, (int)_StencilOperation.Replace);
        break;
    case _UTS_StencilMode.StencilOut:
        //    material.SetInt(ShaderPropStencilNo,0);
        material.SetInt(ShaderPropStencilComp, (int)_StencilCompFunction.NotEqual);
        material.SetInt(ShaderPropStencilOpPass, (int)_StencilOperation.Keep);
        material.SetInt(ShaderPropStencilOpFail, (int)_StencilOperation.Keep);

        break;
}

七大罪中使用使用了深度测试 Greater Equal.(默认是 less Equal)。但这个方式可能在UE4里不太行因为UE4的深度测试是全局的

这个应该需要创建一个MeshProcessor来实现

FSingleLayerWaterPassMeshProcessor::FSingleLayerWaterPassMeshProcessor(const FScene* Scene, const FSceneView* InViewIfDynamicMeshCommand, const FMeshPassProcessorRenderState& InPassDrawRenderState, FMeshPassDrawListContext* InDrawListContext)
	: FMeshPassProcessor(Scene, Scene->GetFeatureLevel(), InViewIfDynamicMeshCommand, InDrawListContext)
	, PassDrawRenderState(InPassDrawRenderState)
{
	if (SingleLayerWaterUsesSimpleShading(Scene->GetShaderPlatform()))
	{
		// Force non opaque, pre multiplied alpha, transparent blend mode because water is going to be blended against scene color (no distortion from texture scene color).
		FRHIBlendState* ForwardSimpleWaterBlendState = TStaticBlendState<CW_RGBA, BO_Add, BF_One, BF_InverseSourceAlpha>::GetRHI();
		PassDrawRenderState.SetBlendState(ForwardSimpleWaterBlendState);
	}
}

//默认是CF_DepthNearOrEqual做这个效果可能就要使用CF_DepthFartherOrEqual
void SetupBasePassState(FExclusiveDepthStencil::Type BasePassDepthStencilAccess, const bool bShaderComplexity, FMeshPassProcessorRenderState& DrawRenderState)
{
	DrawRenderState.SetDepthStencilAccess(BasePassDepthStencilAccess);
	if (bShaderComplexity)
	{
		// Additive blending when shader complexity viewmode is enabled.
		DrawRenderState.SetBlendState(TStaticBlendState<CW_RGBA, BO_Add, BF_One, BF_One, BO_Add, BF_Zero, BF_One>::GetRHI());
		// Disable depth writes as we have a full depth prepass.
		DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<false, CF_DepthNearOrEqual>::GetRHI());
	}
}

设置位置在于FXXXPassMeshProcessor::Process()中的SetDepthStencilStateForBasePass()中。需要bMaskedInEarlyPass为falseGetDepthStencilAccess为DepthRead。

	const bool bMaskedInEarlyPass = (MaterialResource.IsMasked() || Mesh.bDitheredLODTransition) && MaskedInEarlyPass(GShaderPlatformForFeatureLevel[FeatureLevel]);

	if (bEnableReceiveDecalOutput)
	{
		// Set stencil value for this draw call
		// This is effectively extending the GBuffer using the stencil bits
		const uint8 StencilValue = GET_STENCIL_BIT_MASK(RECEIVE_DECAL, PrimitiveSceneProxy ? !!PrimitiveSceneProxy->ReceivesDecals() : 0x00)
			| STENCIL_LIGHTING_CHANNELS_MASK(PrimitiveSceneProxy ? PrimitiveSceneProxy->GetLightingChannelStencilValue() : 0x00);

		if (bMaskedInEarlyPass)
		{
			DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<
				false, CF_Equal,
				true, CF_Always, SO_Keep, SO_Keep, SO_Replace,
				false, CF_Always, SO_Keep, SO_Keep, SO_Keep,
				0xFF, GET_STENCIL_BIT_MASK(RECEIVE_DECAL, 1) | STENCIL_LIGHTING_CHANNELS_MASK(0x7)
			>::GetRHI());
			DrawRenderState.SetStencilRef(StencilValue);
		}
		else if (DrawRenderState.GetDepthStencilAccess() & FExclusiveDepthStencil::DepthWrite)
		{
			DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<
				true, CF_GreaterEqual,
				true, CF_Always, SO_Keep, SO_Keep, SO_Replace,
				false, CF_Always, SO_Keep, SO_Keep, SO_Keep,
				0xFF, GET_STENCIL_BIT_MASK(RECEIVE_DECAL, 1) | STENCIL_LIGHTING_CHANNELS_MASK(0x7)
			>::GetRHI());
			DrawRenderState.SetStencilRef(StencilValue);
		}
		else
		{
			DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<
				false, CF_GreaterEqual,
				true, CF_Always, SO_Keep, SO_Keep, SO_Replace,
				false, CF_Always, SO_Keep, SO_Keep, SO_Keep,
				0xFF, GET_STENCIL_BIT_MASK(RECEIVE_DECAL, 1) | STENCIL_LIGHTING_CHANNELS_MASK(0x7)
			>::GetRHI());
			DrawRenderState.SetStencilRef(StencilValue);
		}
	}
	else if (bMaskedInEarlyPass)
	{
		DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<false, CF_Equal>::GetRHI());
	}

toonshading技术拆解

{% simplemindmap %}

- toonshading技术拆解
    - 描边
        - 外描边:后处理描边 、 Mesh挤出 2种方式原神采用了Mesh挤出。
        - 内描边后处理、SDF描边、本村线 3种方式
		- 使用Mesh基础、后处理、模型绘制。后处理传递Id贴图
    - 分阶着色
        - 二阶化:
        - 多阶化:
        - 自定义次表面(暗部)颜色
        - 颜色过渡(羽化):
        - lut待讨论
    - 高光
        - 高光贴图
        - 高光颜色
        - 参数化高光形状
        - 多层高光
    - 阴影
        - 自定义阴影颜色
        - 自定义阴影范围
        - 自定义阴影形状
        - 阴影过渡sdf阴影
    - 边缘光
        - 边缘光是否受到光照影响
        - 多层边缘光
        - 边缘光衰减(入射角度更明显 or 背光角度更明显)
    - 多光源支持
    - PBR支持
        - PBR和NPR混合
    - AO
    - matcap支持
    - 根据相机角度调整模型(非必要)
    - Shader内编辑法线
        - N=_scale * L + N
    - 眼睛
        - 反射 环境反射或者matcap支持
        - 内阴影 AO实现或者画死的内阴影
        - 瞳孔 瞳孔缩放
        - 视差和效果 凹凸效果
        - 高光 自定义高光形状&位置
        - 高光流动效果
    - 头发
        - 各项异性头发
            - 高光扰动
            - 高光贴图
            - 自定义高光属性
            - 高光天使环
        - 无各项异性头发
            - 自定义高光参数
            - 高光贴图
            - 高光天使环
    - 特殊效果
        - 眉毛/睫毛不受遮挡
        - 自发光
        - 阴影内素描效果
    - 额外效果
        - 后处理 辉光效果
        - 之后提到了SunFlare应该是那个屏幕后处理效果。卡通渲染很依赖体积光所以自然会有好的效果假也没关系假才是对的。谷歌搜SNN Filter    https://www.shadertoy.com/view/MlyfWd
        - 旁边的Kuwahara应该是个类似的算法64采样。

{% endsimplemindmap %}