AI教我UE渲染-学习笔记(5):测试画一个3D三角形

0. 画一个3D三角形

在AI的建议下,下面我将画一个3D空间的三角形,借助 ViewProjectionMatrix(视图投影矩阵)的力量进入3D世界。

让 Claude Opus 4.6 写代码后,它直接就给出了符合要求的代码,一次跑通👍

下面拆解下它的实现

1. 着色器代码

新写一个着色器 SceneLearning.usf 内容如下:

cpp 复制代码
// SceneLearning.usf
// 学习用:画一个 3D 三角形,用 ViewProjectionMatrix 将世界坐标变换到裁剪空间。

#include "../Common.ush"

// VS 参数:世界空间 → 裁剪空间的变换矩阵,由 C++ 侧从 FViewInfo 中获取并传入。
float4x4 ViewProjectionMatrix;

// 顶点着色器:接收世界坐标 Position 和顶点颜色,输出裁剪空间位置和插值颜色。
void MainVS(
	in float3 InPosition : ATTRIBUTE0,
	in float4 InColor    : ATTRIBUTE1,
	out float4 OutColor    : TEXCOORD0,
	out float4 OutPosition : SV_POSITION)
{
	OutPosition = mul(float4(InPosition, 1.0f), ViewProjectionMatrix);
	OutColor = InColor;
}

// 像素着色器:直接输出经光栅化插值后的顶点颜色。
float4 MainPS(
	in float4 InColor : TEXCOORD0) : SV_Target0
{
	return InColor;
}

相关的概念基本之前都已经认识,AI认为就不必细说了,唯一需要补充的一点知识是:(仅供参考)

InPositionfloat3,手动补成 float4(InPosition, 1.0f) ------ w = 1 表示"这是一个空间中的"。如果 w = 0,矩阵乘法时平移分量就不生效,那就变成了"方向"而非"位置"。这是齐次坐标的基本约定,你后续处理法线等方向量时会再碰到 w = 0 的用法。

有些基本的点要注意的是:

  • VS的参数变成了一个矩阵,那么随后C++里shader相关的代码也要变化
  • 接收的顶点数据是一个float3 InPosition与一个float4 InColor,一会儿传入顶点的时候也要匹配

2. C++ 中的着色器定义

如下:

cpp 复制代码
// ========== [Render_Learning] 学习用的 shader 定义 ==========

// 学习用顶点着色器:用 ViewProjectionMatrix 将世界坐标变换到裁剪空间
class FSceneLearningVS : public FGlobalShader
{
	DECLARE_GLOBAL_SHADER(FSceneLearningVS);
	SHADER_USE_PARAMETER_STRUCT(FSceneLearningVS, FGlobalShader);
	BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
		SHADER_PARAMETER(FMatrix44f, ViewProjectionMatrix)
	END_SHADER_PARAMETER_STRUCT()
};
IMPLEMENT_GLOBAL_SHADER(FSceneLearningVS, "/Engine/Private/Tools/SceneLearning.usf", "MainVS", SF_Vertex);

// 学习用像素着色器:直接输出顶点颜色
class FSceneLearningPS : public FGlobalShader
{
	DECLARE_GLOBAL_SHADER(FSceneLearningPS);
	SHADER_USE_PARAMETER_STRUCT(FSceneLearningPS, FGlobalShader);
	BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
		RENDER_TARGET_BINDING_SLOTS()
	END_SHADER_PARAMETER_STRUCT()
};
IMPLEMENT_GLOBAL_SHADER(FSceneLearningPS, "/Engine/Private/Tools/SceneLearning.usf", "MainPS", SF_Pixel);

相比之前的着色器定义,VS中多了个SHADER_PARAMETER(FMatrix44f, ViewProjectionMatrix)这个参数。而PS中则不再需要参数了。

3. 顶点描述

接下来,定义顶点的数据结构:

cpp 复制代码
// 学习用顶点格式:世界空间位置 + 颜色
struct FLearningVertex
{
	FVector3f Position;
	FVector4f Color;
};

然后是与之对应的顶点描述:

cpp 复制代码
// 学习用顶点声明:告诉 GPU 如何从 FLearningVertex 里解析出 ATTRIBUTE0(Position) 和 ATTRIBUTE1(Color)
class FLearningVertexDeclaration : public FRenderResource
{
public:
	FVertexDeclarationRHIRef VertexDeclarationRHI;
	virtual void InitRHI(FRHICommandListBase& RHICmdList) override
	{
		FVertexDeclarationElementList Elements;
		uint16 Stride = sizeof(FLearningVertex);
		Elements.Add(FVertexElement(0, STRUCT_OFFSET(FLearningVertex, Position), VET_Float3, 0, Stride));
		Elements.Add(FVertexElement(0, STRUCT_OFFSET(FLearningVertex, Color),    VET_Float4, 1, Stride));
		VertexDeclarationRHI = PipelineStateCache::GetOrCreateVertexDeclaration(Elements);
	}
	virtual void ReleaseRHI() override { VertexDeclarationRHI.SafeRelease(); }
};

以及这个顶点描述的全局对象

cpp 复制代码
static TGlobalResource<FLearningVertexDeclaration> GLearningVertexDeclaration;

这里问了AI:

下面我想了解下FRenderResource这个概念,顶点描述为何继承它,或者说继承他带来了哪些能力。

FRenderResource和之前我了解的FRDGResource有什么区别,有何不同的分工?

AI 回答:(仅供参考)

好问题。两者名字里都有 "Resource",但完全不是一回事


FRenderResource:持久的 RHI 资源容器

FRenderResource 是一个生命周期跟随引擎的基类,核心能力是一对虚函数:

  • InitRHI() :引擎启动 / RHI 就绪时被调用,在里面创建 GPU 资源(顶点缓冲、索引缓冲、声明等)。
  • ReleaseRHI():引擎关闭或 RHI 重置时被调用,释放资源。

你的 FLearningVertexDeclaration 继承它,就获得了这个能力:InitRHI 里创建顶点声明,引擎负责在合适时机调用 InitRHI / ReleaseRHI,你不需要手动管理创建和销毁的时机。

配合 TGlobalResource<> 使用时,效果是:声明一个全局静态变量,引擎启动时自动 InitRHI,关闭时自动 ReleaseRHI。这就是你代码里这行的含义:

4133:4133:Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp 复制代码
static TGlobalResource<FLearningVertexDeclaration> GLearningVertexDeclaration;

同理,FVertexBufferFIndexBufferFTexture 等都继承自 FRenderResource,用的也是这套机制。


FRDGResource:RDG 图里的临时节点

FRDGResourceRDG(Render Dependency Graph)内部 的基类,代表一帧内 的临时资源节点(如 FRDGTextureFRDGBuffer)。它的职责是:

  • 被 RDG 追踪依赖关系(哪个 pass 读、哪个 pass 写)
  • 由 RDG 自动管理 barrier / 状态转换
  • 一帧结束后就不存在了(下一帧重新创建)

你之前用的 FRDGTextureRef(如 SceneTextures.Color.Target)就是 FRDGResource 的子类。


对比

FRenderResource FRDGResource
生命周期 跟随引擎,持久存在 一帧内,用完即弃
谁管理 自己写 InitRHI/ReleaseRHI,或用 TGlobalResource 自动管 RDG 框架(GraphBuilder)自动管
典型子类 FVertexBufferFIndexBufferFFilterVertexDeclaration FRDGTextureFRDGBuffer
用在哪 不变的静态数据(全屏四边形 VB、你的三角形 VB、顶点声明等) 每帧临时的渲染纹理/缓冲(SceneColor、深度、GBuffer 等)
和 RHI 的关系 直接持有 RHI 对象(如 VertexBufferRHI 内部会引用或分配底层 RHI 资源,但你不直接操作

一句话:FRenderResource 是"引擎级别的持久 GPU 资源管理";FRDGResource 是"RDG 图里一帧内的临时资源节点"。你的顶点缓冲和顶点声明不会每帧变,所以用 FRenderResource + TGlobalResource;而 SceneColor 等每帧由 RDG 管理分配释放,所以是 FRDGResource

4. 顶点数据

接下来,就是我们3D三角形的顶点数据了:

cpp 复制代码
#include "RHIResourceUtils.h"

// 学习用三角形顶点缓冲:3 个顶点,世界坐标,各自带一种颜色
static const FLearningVertex GLearningTriangleData[] =
{
	// Position (世界空间,单位 cm)              Color (R,G,B,A)
	{ FVector3f(-200.0f,    0.0f,   0.0f), FVector4f(1, 0, 0, 1) },  // 左  - 红
	{ FVector3f( 200.0f,    0.0f,   0.0f), FVector4f(0, 1, 0, 1) },  // 右  - 绿
	{ FVector3f(   0.0f,    0.0f, 400.0f), FVector4f(0, 0, 1, 1) },  // 顶  - 蓝
};

class FLearningTriangleVertexBuffer : public FVertexBuffer
{
public:
	void InitRHI(FRHICommandListBase& RHICmdList) override
	{
		VertexBufferRHI = UE::RHIResourceUtils::CreateVertexBufferFromArray(
			RHICmdList, TEXT("LearningTriangleVB"), EBufferUsageFlags::Static,
			MakeConstArrayView(GLearningTriangleData));
	}
};
static TGlobalResource<FLearningTriangleVertexBuffer> GLearningTriangleVB;

我注意到 FVertexBuffer 也是一个FRenderResource

于是问AI:

FVertexBuffer也是一个FRenderResource,为什么我的顶点数据缓存不直接继承FRenderResource?或者说继承FVertexBuffer带来了哪些便利?

AI回答(仅供参考):

FVertexBuffer 本身非常薄,你看它的全部内容就这几行。带来的便利主要是两点:

  1. 自带 VertexBufferRHI 成员 :你 InitRHI 里直接往 VertexBufferRHI 赋值就行。如果直接继承 FRenderResource,你得自己声明一个 FBufferRHIRef 成员来存。

  2. ReleaseRHI() 已经帮你写好了 :它会自动释放 VertexBufferRHI。你的 FLearningTriangleVertexBuffer 没有 override ReleaseRHI,就是因为父类已经处理了。如果直接继承 FRenderResource,你得自己写 ReleaseRHI 来释放缓冲。

本质上就是省了声明成员 + 省了写释放逻辑,外加语义上更清晰(一看就知道这是个顶点缓冲)。不继承它也完全能工作,只是要多写几行。

5. 画三角形的pass

它的实现如下:

cpp 复制代码
// ========== [Render_Learning] 画 3D 三角形的 pass ==========

// 在 OutputTexture 上画一个硬编码的 3D 三角形。
// ViewProjectionMatrix 把世界坐标变换到裁剪空间,由 Render_Learning 从 FViewInfo 中获取并传入。
static void AddTrianglePass_Learning(
	FRDGBuilder& GraphBuilder,
	const FGlobalShaderMap* ShaderMap,
	const FMatrix& ViewProjectionMatrix,
	FRDGTextureRef OutputTexture)
{
	// 1. 从 GlobalShaderMap 中取出 VS 和 PS
	TShaderMapRef<FSceneLearningVS> VertexShader(ShaderMap);
	TShaderMapRef<FSceneLearningPS> PixelShader(ShaderMap);

	// 2. 填写 PS 参数(渲染目标),ELoad 保留之前清屏的背景色
	FSceneLearningPS::FParameters* PSParameters = GraphBuilder.AllocParameters<FSceneLearningPS::FParameters>();
	PSParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::ELoad);

	// 3. 填写 VS 参数(ViewProjectionMatrix)
	FSceneLearningVS::FParameters VSParameters;
	VSParameters.ViewProjectionMatrix = FMatrix44f(ViewProjectionMatrix);

	const FIntPoint OutputExtent = OutputTexture->Desc.Extent;

	// 4. 向 RDG 添加一个光栅化 pass
	GraphBuilder.AddPass(
		RDG_EVENT_NAME("LearningTriangle"),
		PSParameters,
		ERDGPassFlags::Raster,
		[VertexShader, PixelShader, PSParameters, VSParameters, OutputExtent](FRDGAsyncTask, FRHICommandList& RHICmdList)
		{
			// 5. 设置视口
			RHICmdList.SetViewport(0.0f, 0.0f, 0.0f, (float)OutputExtent.X, (float)OutputExtent.Y, 1.0f);

			// 6. 构建 PSO
			FGraphicsPipelineStateInitializer GraphicsPSOInit;
			RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
			GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
			GraphicsPSOInit.RasterizerState = TStaticRasterizerState<FM_Solid, CM_None>::GetRHI();  // 关闭背面剔除,双面可见
			GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();
			GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GLearningVertexDeclaration.VertexDeclarationRHI;
			GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
			GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
			GraphicsPSOInit.PrimitiveType = PT_TriangleList;
			SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0);

			// 7. 绑定 VS 和 PS 的参数
			SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), VSParameters);
			SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), *PSParameters);

			// 8. 绑定三角形的顶点缓冲并发出 DrawCall(1 个三角形 = 3 个顶点)
			RHICmdList.SetStreamSource(0, GLearningTriangleVB.VertexBufferRHI, 0);
			RHICmdList.DrawPrimitive(0, 1, 1);  // StartVertex, NumPrimitives(1 三角形), NumInstances
		}
	);
}

他和之前画贴图的pass相似度很高:

让AI盘点下改动点,它的回答:(仅供参考)

逐项对比,一共有 6 处不同:


1. 函数参数:InputTextureViewProjectionMatrix

FRDGTextureRef InputTexture const FMatrix& ViewProjectionMatrix

旧版需要一张输入纹理 来采样;新版不再采样纹理,取而代之的是一个变换矩阵 来做 3D 变换。OutputTexture 两边都有,没变。


2. PS 参数:从"纹理+采样器+RT"简化为"只有 RT"

旧:

cpp 复制代码
Parameters->InputTexture = InputTexture;
Parameters->InputSampler = TStaticSamplerState<SF_Bilinear>::GetRHI();
Parameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::ENoAction);

新:

cpp 复制代码
PSParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::ELoad);

两点变化:

  • PS 不再需要 InputTextureInputSampler(因为 PS 现在只输出顶点颜色,不采样纹理)。
  • ERenderTargetLoadActionENoAction 改成了 ELoad:旧版全屏四边形会覆盖所有像素所以不需要加载旧内容;新版三角形只覆盖部分像素,需要保留背景色

3. 新增 VS 参数

旧: 没有 VS 参数,也没有 SetShaderParameters 给 VS。

新:

cpp 复制代码
FSceneLearningVS::FParameters VSParameters;
VSParameters.ViewProjectionMatrix = FMatrix44f(ViewProjectionMatrix);

以及 lambda 里多了一行:

cpp 复制代码
SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), VSParameters);

旧版 VS 不需要任何外部数据(全屏四边形位置是固定的 0~1 映射);新版 VS 需要 ViewProjectionMatrix 来做世界→裁剪空间变换。


4. PSO:光栅化状态和顶点声明

旧:

cpp 复制代码
GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;

新:

cpp 复制代码
GraphicsPSOInit.RasterizerState = TStaticRasterizerState<FM_Solid, CM_None>::GetRHI();
GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GLearningVertexDeclaration.VertexDeclarationRHI;

两点变化:

  • 光栅化状态加了 CM_None(关闭背面剔除)。旧版全屏四边形永远正面朝屏幕不需要关心;新版 3D 三角形从背面看也要可见。
  • 顶点声明从 GFilterVertexDeclaration(Position float4 + UV float2)换成了 GLearningVertexDeclaration(Position float3 + Color float4),匹配新的顶点数据格式。

5. 顶点缓冲

旧:

cpp 复制代码
RHICmdList.SetStreamSource(0, GScreenRectangleVertexBuffer.VertexBufferRHI, 0);

新:

cpp 复制代码
RHICmdList.SetStreamSource(0, GLearningTriangleVB.VertexBufferRHI, 0);

从引擎全局的全屏矩形缓冲,换成了自定义的 3D 三角形缓冲。


6. DrawCall:从索引绘制变成直接绘制

旧:

cpp 复制代码
RHICmdList.DrawIndexedPrimitive(
    GScreenRectangleIndexBuffer.IndexBufferRHI,
    0, 0, 4,   // 4 顶点
    0, 2, 1);  // 2 三角形

新:

cpp 复制代码
RHICmdList.DrawPrimitive(0, 1, 1);  // 1 三角形

旧版用 4 个顶点 + 索引缓冲拼 2 个三角形(矩形);新版只有 3 个顶点、1 个三角形,不需要索引缓冲,直接用 DrawPrimitive


总结: 整体结构(取 shader → 填参数 → AddPass → 设视口 → 建 PSO → 绑参数 → DrawCall)完全没变,变的只是"画什么"和"用什么数据":从全屏纹理拷贝变成了 3D 三角形绘制。

6. 调用画pass

最后,就是在Render_Learning中调用了

cpp 复制代码
...
FRDGTextureRef ViewFamilyTexture = TryCreateViewFamilyTexture(GraphBuilder, ViewFamily);
if (ViewFamilyTexture && Views.Num() > 0)
{
	const FViewInfo& View = Views[0];

	// 从 FViewInfo 获取 ViewProjectionMatrix(世界空间 → 裁剪空间),和原版 Render 取矩阵的方式一致。
	const FMatrix ViewProjectionMatrix = View.ViewMatrices.GetViewProjectionMatrix();

	// 在 SceneColor 上画一个 3D 三角形
	AddTrianglePass_Learning(GraphBuilder, View.ShaderMap, ViewProjectionMatrix, SceneTextures.Color.Target);

	// 把 SceneColor 拷贝到 ViewFamilyTexture(最终上屏)
	AddDrawTexturePass(GraphBuilder, View.ShaderMap, SceneTextures.Color.Target, ViewFamilyTexture, FRDGDrawTextureInfo());
}

这里主要就是获得 ViewProjectionMatrix 这个视图投影矩阵了。

这个数据由 ViewMatrix × ProjectionMatrix 这俩矩阵计算而来:

7. 让AI重新梳理一遍视图投影矩阵的计算过程(仅供参考)

好,下面结合代码从头到尾走一遍。


第一步:C++ 侧获取矩阵

4267:4267:Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp 复制代码
const FMatrix ViewProjectionMatrix = View.ViewMatrices.GetViewProjectionMatrix();

这个矩阵在引擎内部是这样算出来的:

757:757:Engine/Source/Runtime/Engine/Public/SceneView.h 复制代码
ViewProjectionMatrix = GetViewMatrix() * GetProjectionMatrix();

也就是两个矩阵相乘。分别看这两个矩阵的含义:

ViewMatrix(世界 → 相机空间)

假设相机在世界中的位置是 EEE,相机的三个正交轴(右、上、前等)是 Xc,Yc,ZcX_c, Y_c, Z_cXc,Yc,Zc。ViewMatrix 做的是"把整个世界搬到相机的视角":

V=[Xc.xYc.xZc.x0Xc.yYc.yZc.y0Xc.zYc.zZc.z0−(E⋅Xc)−(E⋅Yc)−(E⋅Zc)1] V = \begin{bmatrix} X_c.x & Y_c.x & Z_c.x & 0 \\ X_c.y & Y_c.y & Z_c.y & 0 \\ X_c.z & Y_c.z & Z_c.z & 0 \\ -(E \cdot X_c) & -(E \cdot Y_c) & -(E \cdot Z_c) & 1 \end{bmatrix} V= Xc.xXc.yXc.z−(E⋅Xc)Yc.xYc.yYc.z−(E⋅Yc)Zc.xZc.yZc.z−(E⋅Zc)0001

最后一行里的 −(E⋅Xc)-(E \cdot X_c)−(E⋅Xc) 等是点积,效果是先平移(让相机成为原点),再旋转(让相机轴对齐坐标轴)。

变换后:相机在原点,看向某个坐标轴方向

ProjectionMatrix(相机空间 → 裁剪空间)

透视投影(你在编辑器里用的就是这种)。输入参数:垂直视场角 fovfovfov,宽高比 aaa,近裁剪面 nnn,远裁剪面 fff。

令 h=1tan⁡(fov/2)h = \frac{1}{\tan(fov/2)}h=tan(fov/2)1,矩阵为(D3D 风格,深度映射到 [0,1][0,1][0,1]):

P=[h/a0000h0000ff−n100−nff−n0] P = \begin{bmatrix} h/a & 0 & 0 & 0 \\ 0 & h & 0 & 0 \\ 0 & 0 & \frac{f}{f-n} & 1 \\ 0 & 0 & \frac{-nf}{f-n} & 0 \end{bmatrix} P= h/a0000h0000f−nff−n−nf0010

关键效果:

  • x, y 列:按 FOV 和宽高比缩放(视角越大,物体越小)
  • z 列 :把 [n,f][n, f][n,f] 深度范围非线性映射到 [0,1][0, 1][0,1]
  • 第四列的 1 :让输出的 w=zcameraw = z_{camera}w=zcamera(原始深度值),为后续硬件做 ÷w\div w÷w 做准备

两者相乘得到 ViewProjectionMatrix世界空间 → 裁剪空间,一步到位


第二步:C++ 侧传给 VS

4199:4200:Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp 复制代码
FSceneLearningVS::FParameters VSParameters;
VSParameters.ViewProjectionMatrix = FMatrix44f(ViewProjectionMatrix);

FMatrix(双精度)转成 FMatrix44f(单精度),存入 VS 参数,执行时由 SetShaderParameters 写入 GPU 常量缓冲。


第三步:Shader 里做变换

16:16:Engine/Shaders/Private/Tools/SceneLearning.usf 复制代码
OutPosition = mul(float4(InPosition, 1.0f), ViewProjectionMatrix);

以你的红色顶点 (−200,0,0)(-200, 0, 0)(−200,0,0) 为例,这一行做的计算是:

−200001\]×(V×P)=\[xcyczcwc\] \\begin{bmatrix} -200 \& 0 \& 0 \& 1 \\end{bmatrix} \\times (V \\times P) = \\begin{bmatrix} x_c \& y_c \& z_c \& w_c \\end{bmatrix} \[−200001\]×(V×P)=\[xcyczcwc

输出是一个 float4,即裁剪空间坐标 (xc,yc,zc,wc)(x_c, y_c, z_c, w_c)(xc,yc,zc,wc)。


第四步:GPU 硬件自动完成

VS 输出 SV_POSITION 之后,GPU 自动做以下步骤(不在你的代码里,硬件完成):

透视除法(裁剪空间 → NDC):

xndc=xcwc,yndc=ycwc,zndc=zcwc x_{ndc} = \frac{x_c}{w_c}, \quad y_{ndc} = \frac{y_c}{w_c}, \quad z_{ndc} = \frac{z_c}{w_c} xndc=wcxc,yndc=wcyc,zndc=wczc

xndc,yndcx_{ndc}, y_{ndc}xndc,yndc 落在 [−1,1][-1, 1][−1,1],zndcz_{ndc}zndc 落在 [0,1][0, 1][0,1]。

视口映射(NDC → 像素坐标):

xpixel=(xndc+1)2×Width,ypixel=(1−yndc)2×Height x_{pixel} = \frac{(x_{ndc} + 1)}{2} \times Width, \quad y_{pixel} = \frac{(1 - y_{ndc})}{2} \times Height xpixel=2(xndc+1)×Width,ypixel=2(1−yndc)×Height

对应你代码里设的视口:

4212:4212:Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp 复制代码
RHICmdList.SetViewport(0.0f, 0.0f, 0.0f, (float)OutputExtent.X, (float)OutputExtent.Y, 1.0f);

之后就进入光栅化 → 对三角形覆盖的每个像素调用 PS → 输出颜色到 RT。


完整链路一句话 :C++ 从 FViewInfo 取 V×PV \times PV×P → 传给 VS → VS 对每个世界坐标顶点乘这个矩阵得到裁剪坐标 → GPU 硬件做 ÷w\div w÷w 和视口映射得到像素位置 → 光栅化 → PS 输出颜色。

相关推荐
暗光之痕3 天前
Unreal5 研究笔记 蓝图自定义节点
笔记·unreal engine
YakSue6 天前
AI教我UE渲染-学习笔记(4):测试新写个全屏复制贴图的shader与pass
unreal engine
JCHwa10 天前
UE5 GAS 源码深度解析 | 第2篇:AttributeSet 源码导读
游戏开发·unreal engine
侑虎科技1 个月前
在UE5中,预测脚步IK实现-PredictFootIK
性能优化·unreal engine
暗光之痕2 个月前
Unreal5研究笔记 Actor的生命周期函数
笔记·unreal engine
哎呦哥哥和巨炮叔叔2 个月前
Unreal Engine 是否支持光线追踪?UE5 光线追踪原理与性能解析
ue5·unreal engine·光线追踪·lumen·实时渲染·渲染101云渲染·ue云渲染
DoomGT2 个月前
Physics Simulation - Hit Event的触发机制
ue5·游戏引擎·虚幻·虚幻引擎·unreal engine
郝学胜-神的一滴2 个月前
QtOpenGL多线程渲染方案深度解析
c++·qt·unity·游戏引擎·godot·图形渲染·unreal engine
归真仙人3 个月前
【UE】UMG安卓相关问题
android·ue5·游戏引擎·ue4·虚幻·unreal engine