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认为就不必细说了,唯一需要补充的一点知识是:(仅供参考)
InPosition是float3,手动补成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;
同理,FVertexBuffer、FIndexBuffer、FTexture 等都继承自 FRenderResource,用的也是这套机制。
FRDGResource:RDG 图里的临时节点
FRDGResource 是 RDG(Render Dependency Graph)内部 的基类,代表一帧内 的临时资源节点(如 FRDGTexture、FRDGBuffer)。它的职责是:
- 被 RDG 追踪依赖关系(哪个 pass 读、哪个 pass 写)
- 由 RDG 自动管理 barrier / 状态转换
- 一帧结束后就不存在了(下一帧重新创建)
你之前用的 FRDGTextureRef(如 SceneTextures.Color.Target)就是 FRDGResource 的子类。
对比
| FRenderResource | FRDGResource | |
|---|---|---|
| 生命周期 | 跟随引擎,持久存在 | 一帧内,用完即弃 |
| 谁管理 | 自己写 InitRHI/ReleaseRHI,或用 TGlobalResource 自动管 |
RDG 框架(GraphBuilder)自动管 |
| 典型子类 | FVertexBuffer、FIndexBuffer、FFilterVertexDeclaration |
FRDGTexture、FRDGBuffer |
| 用在哪 | 不变的静态数据(全屏四边形 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 本身非常薄,你看它的全部内容就这几行。带来的便利主要是两点:
-
自带
VertexBufferRHI成员 :你InitRHI里直接往VertexBufferRHI赋值就行。如果直接继承FRenderResource,你得自己声明一个FBufferRHIRef成员来存。 -
ReleaseRHI()已经帮你写好了 :它会自动释放VertexBufferRHI。你的FLearningTriangleVertexBuffer没有 overrideReleaseRHI,就是因为父类已经处理了。如果直接继承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. 函数参数:InputTexture → ViewProjectionMatrix
| 旧 | 新 |
|---|---|
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 不再需要
InputTexture和InputSampler(因为 PS 现在只输出顶点颜色,不采样纹理)。 ERenderTargetLoadAction从ENoAction改成了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 输出颜色。