第六章图形 API 巡礼——横向对比一句话概括同一件事六套 API 用六种方言说——但核心概念是相通的。生活类比中文、英文、日文都能表达给我一杯水语法不同但意思一样。⏱ 30 秒概览六套 API 表达 RT 的方式不同但本质相通OpenGL用 FBO容器 Attachment挂载点 Renderbuffer/TextureDX11用 Texture2D RTV/DSVViewOMSetRenderTargetsDX12加入 Descriptor Heap 和显式 Resource BarrierVulkan最繁琐Image → ImageView → Framebuffer → RenderPassVulkan 1.3 简化为 Dynamic RenderingMetal用 RenderPassDescriptor 统一描述 loadAction/storeAction支持 Memoryless 模式省显存WebGPU概念接近 Metal/Vulkan 的混合。核心映射关系所有 API 都有资源 视图 绑定 状态管理四层。掌握一套迁移其余只需查表。本章不是六份 API 手册。我们的目标是以横向对比为主线让你看到不同 API 如何表达同一个概念建立跨 API 的思维迁移能力。6.1 OpenGL / OpenGL ES核心概念OpenGL 中的 RenderTarget 通过FBOFramebuffer Object管理。FBO 是一个容器可以挂载多个Attachment。默认 Framebuffer vs FBOOpenGL 有一个默认 FramebufferID 为 0就是窗口的后缓冲。你不需要创建它它随窗口自动存在。自定义 FBO 需要手动创建GLuint fbo;glGenFramebuffers(1,fbo);glBindFramebuffer(GL_FRAMEBUFFER,fbo);AttachmentRenderbuffer vs TextureFBO 的每个挂载点可以挂两种东西Renderbuffer由glGenRenderbuffers创建。只能作为 Attachment 被 GPU 写入不能被 Shader 采样。适合 Depth/Stencil 这种只写不读的场景。GLuint depthRB;glGenRenderbuffers(1,depthRB);glBindRenderbuffer(GL_RENDERBUFFER,depthRB);glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH24_STENCIL8,512,512);glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_STENCIL_ATTACHMENT,GL_RENDERBUFFER,depthRB);Texture由glGenTextures创建。既能被写入作为 FBO Attachment又能被 Shader 采样。最常用。GLuint colorTex;glGenTextures(1,colorTex);glBindTexture(GL_TEXTURE_2D,colorTex);glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,512,512,0,GL_RGBA,GL_UNSIGNED_BYTE,NULL);glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,colorTex,0);完整性检查OpenGL 要求 FBO 的所有 Attachment 满足一定条件尺寸一致、格式兼容等否则 FBO “不完整”IncompleteGLenum statusglCheckFramebufferStatus(GL_FRAMEBUFFER);if(status!GL_FRAMEBUFFER_COMPLETE){// 出错了常见原因尺寸不一致、格式不支持、缺少 Attachment}MRTOpenGL 支持多个 Color AttachmentglFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,tex0,0);glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT1,GL_TEXTURE_2D,tex1,0);GLenum drawBuffers[]{GL_COLOR_ATTACHMENT0,GL_COLOR_ATTACHMENT1};glDrawBuffers(2,drawBuffers);Fragment Shader 中layout(location 0) out vec4 fragColor0; layout(location 1) out vec4 fragColor1;典型坑忘记调用glDrawBuffers→ 只有COLOR_ATTACHMENT0有输出MSAA FBO 的所有 Attachment 必须有相同的 sample count否则GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE默认 Framebuffer 的 Depth 和自定义 FBO 的 Depth 是独立的——切 FBO 时深度不会自动带过去6.2 DirectX 11核心概念DX11 采用Resource View模型底层资源ID3D11Texture2D和对资源的看法View分离。Resource 创建D3D11_TEXTURE2D_DESC desc{};desc.Width1920;desc.Height1080;desc.MipLevels1;desc.ArraySize1;desc.FormatDXGI_FORMAT_R8G8B8A8_UNORM;desc.SampleDesc.Count1;// 无 MSAAdesc.UsageD3D11_USAGE_DEFAULT;desc.BindFlagsD3D11_BIND_RENDER_TARGET|D3D11_BIND_SHADER_RESOURCE;ID3D11Texture2D*texture;device-CreateTexture2D(desc,nullptr,texture);注意BindFlags同时指定了RENDER_TARGET和SHADER_RESOURCE——这块显存既能写又能读。View 创建// RTV以渲染目标的视角看这块资源ID3D11RenderTargetView*rtv;device-CreateRenderTargetView(texture,nullptr,rtv);// SRV以纹理采样的视角看这块资源ID3D11ShaderResourceView*srv;device-CreateShaderResourceView(texture,nullptr,srv);// DSV对深度纹理ID3D11DepthStencilView*dsv;device-CreateDepthStencilView(depthTexture,nullptr,dsv);绑定// 设置渲染目标最多 8 个 RTV 1 个 DSVID3D11RenderTargetView*rtvs[]{rtv0,rtv1,rtv2,rtv3};context-OMSetRenderTargets(4,rtvs,dsv);// 清除floatclearColor[]{0,0,0,1};context-ClearRenderTargetView(rtv0,clearColor);context-ClearDepthStencilView(dsv,D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL,1.0f,0);DX11 的隐式管理DX11 有一个特殊行为如果你把同一个资源同时绑为 RTV 和 SRVDX11 会自动解绑 SRV并在 Debug 层输出 WarningD3D11 WARNING: ID3D11DeviceContext::PSSetShaderResources: Resource being set to PS shader resource slot 0 is still bound on output! Forcing to NULL.这是驱动在帮你防止读写互斥。用完 RT 后记得把 RTV 解绑或设为 nullptr再绑 SRV。6.3 DirectX 12核心概念DX12 把所有隐式管理都去掉了。你需要手动做创建资源、创建 Descriptor、管理状态转换、同步。Descriptor Heap 与 RTVDX12 的 View 不是独立对象而是存放在Descriptor Heap中的条目。RTV 有专用的 Descriptor Heap。// 1. 创建 RTV Descriptor HeapD3D12_DESCRIPTOR_HEAP_DESC heapDesc{};heapDesc.NumDescriptors4;// 最多 4 个 RTVheapDesc.TypeD3D12_DESCRIPTOR_HEAP_TYPE_RTV;device-CreateDescriptorHeap(heapDesc,IID_PPV_ARGS(rtvHeap));// 2. 创建纹理资源D3D12_RESOURCE_DESC texDesc{};texDesc.DimensionD3D12_RESOURCE_DIMENSION_TEXTURE2D;texDesc.Width1920;texDesc.Height1080;texDesc.FormatDXGI_FORMAT_R8G8B8A8_UNORM;texDesc.FlagsD3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;// ...ID3D12Resource*texture;device-CreateCommittedResource(heapProps,D3D12_HEAP_FLAG_NONE,texDesc,D3D12_RESOURCE_STATE_RENDER_TARGET,clearValue,IID_PPV_ARGS(texture));// 3. 在 Heap 中创建 RTV Descriptordevice-CreateRenderTargetView(texture,nullptr,rtvHeap-GetCPUDescriptorHandleForHeapStart());Resource Barrier——显式状态转换这是 DX12 与 DX11 最大的区别之一// 从 RT 状态转到纹理状态D3D12_RESOURCE_BARRIER barrier{};barrier.TypeD3D12_RESOURCE_BARRIER_TYPE_TRANSITION;barrier.Transition.pResourcetexture;barrier.Transition.StateBeforeD3D12_RESOURCE_STATE_RENDER_TARGET;barrier.Transition.StateAfterD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;barrier.Transition.SubresourceD3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;cmdList-ResourceBarrier(1,barrier);忘了 Barrier → 验证层报错Release 模式下可能出现画面错乱。渲染// 设置 RTcmdList-OMSetRenderTargets(1,rtvHandle,FALSE,dsvHandle);// 清除cmdList-ClearRenderTargetView(rtvHandle,clearColor,0,nullptr);cmdList-ClearDepthStencilView(dsvHandle,D3D12_CLEAR_FLAG_DEPTH,1.0f,0,0,nullptr);// 画东西...// 转换状态cmdList-ResourceBarrier(1,barrier);// 现在可以在后续 Pass 中以纹理身份采样了6.4 Vulkan核心概念Vulkan 的 RT 体系涉及多个层次VkImage物理资源 → VkImageView资源的视角 → VkFramebuffer把多个 ImageView 绑在一起 → VkRenderPass描述 Attachment 如何被使用创建 ImageVkImageCreateInfo imageInfo{};imageInfo.imageTypeVK_IMAGE_TYPE_2D;imageInfo.formatVK_FORMAT_R8G8B8A8_UNORM;imageInfo.extent{1920,1080,1};imageInfo.mipLevels1;imageInfo.arrayLayers1;imageInfo.samplesVK_SAMPLE_COUNT_1_BIT;imageInfo.tilingVK_IMAGE_TILING_OPTIMAL;imageInfo.usageVK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT|VK_IMAGE_USAGE_SAMPLED_BIT;VkImage image;vkCreateImage(device,imageInfo,nullptr,image);// 还需要分配内存并绑定省略创建 ImageViewVkImageViewCreateInfo viewInfo{};viewInfo.imageimage;viewInfo.viewTypeVK_IMAGE_VIEW_TYPE_2D;viewInfo.formatVK_FORMAT_R8G8B8A8_UNORM;viewInfo.subresourceRange.aspectMaskVK_IMAGE_ASPECT_COLOR_BIT;viewInfo.subresourceRange.levelCount1;viewInfo.subresourceRange.layerCount1;VkImageView imageView;vkCreateImageView(device,viewInfo,nullptr,imageView);RenderPass FramebufferVulkan 要求你先描述整个 Render Pass 的结构——有多少 Attachment、每个 Attachment 的格式、loadOp、storeOp、Layout 转换VkAttachmentDescription colorAttachment{};colorAttachment.formatVK_FORMAT_R8G8B8A8_UNORM;colorAttachment.samplesVK_SAMPLE_COUNT_1_BIT;colorAttachment.loadOpVK_ATTACHMENT_LOAD_OP_CLEAR;colorAttachment.storeOpVK_ATTACHMENT_STORE_OP_STORE;colorAttachment.initialLayoutVK_IMAGE_LAYOUT_UNDEFINED;colorAttachment.finalLayoutVK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;VkAttachmentReference colorRef{0,VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};VkSubpassDescription subpass{};subpass.colorAttachmentCount1;subpass.pColorAttachmentscolorRef;VkRenderPassCreateInfo rpInfo{};rpInfo.attachmentCount1;rpInfo.pAttachmentscolorAttachment;rpInfo.subpassCount1;rpInfo.pSubpassessubpass;VkRenderPass renderPass;vkCreateRenderPass(device,rpInfo,nullptr,renderPass);然后创建 Framebuffer 把 ImageView 和 RenderPass 绑在一起VkFramebufferCreateInfo fbInfo{};fbInfo.renderPassrenderPass;fbInfo.attachmentCount1;fbInfo.pAttachmentsimageView;fbInfo.width1920;fbInfo.height1080;fbInfo.layers1;VkFramebuffer framebuffer;vkCreateFramebuffer(device,fbInfo,nullptr,framebuffer);渲染VkRenderPassBeginInfo beginInfo{};beginInfo.renderPassrenderPass;beginInfo.framebufferframebuffer;beginInfo.renderArea{{0,0},{1920,1080}};beginInfo.clearValueCount1;beginInfo.pClearValuesclearValue;vkCmdBeginRenderPass(cmdBuf,beginInfo,VK_SUBPASS_CONTENTS_INLINE);// Draw calls...vkCmdEndRenderPass(cmdBuf);// 结束时 GPU 自动做 Layout TransitionfinalLayoutDynamic RenderingVK_KHR_dynamic_rendering社区反馈 VkRenderPass VkFramebuffer 过于复杂。Vulkan 1.3 内置了 Dynamic RenderingVkRenderingAttachmentInfo colorAttachInfo{};colorAttachInfo.imageViewimageView;colorAttachInfo.imageLayoutVK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;colorAttachInfo.loadOpVK_ATTACHMENT_LOAD_OP_CLEAR;colorAttachInfo.storeOpVK_ATTACHMENT_STORE_OP_STORE;colorAttachInfo.clearValueclearValue;VkRenderingInfo renderingInfo{};renderingInfo.renderArea{{0,0},{1920,1080}};renderingInfo.layerCount1;renderingInfo.colorAttachmentCount1;renderingInfo.pColorAttachmentscolorAttachInfo;vkCmdBeginRendering(cmdBuf,renderingInfo);// Draw calls...vkCmdEndRendering(cmdBuf);不再需要预先创建 VkRenderPass 和 VkFramebuffer。在桌面端更灵活。但在移动端传统 VkRenderPass Subpass 仍然是最优选择因为 Subpass 合并的优化需要完整的 RenderPass 结构信息。实践建议如果你的项目 Minimum Spec 是 Vulkan 1.3桌面端 2022 硬件基本全覆盖新代码优先用 Dynamic Rendering——代码量减少约 40%维护成本大幅降低。仅在移动端需要 Subpass 合并优化的 Pass 才创建完整的VkRenderPass。许多现代引擎如 Godot 4.x已将 Dynamic Rendering 作为桌面端默认路径。6.5 Metal核心概念Metal 的 RT 设计最直觉概念最少MTLTexture资源可作为 Attachment → MTLRenderPassDescriptor描述 Attachment loadAction/storeAction → MTLRenderCommandEncoder在此 Encoder 下执行 Draw Call创建纹理lettexDescMTLTextureDescriptor.texture2DDescriptor(pixelFormat:.rgba8Unorm,width:1920,height:1080,mipmapped:false)texDesc.usage[.renderTarget,.shaderRead]texDesc.storageMode.private// GPU 专用lettexturedevice.makeTexture(descriptor:texDesc)!Render Pass DescriptorletrpDescMTLRenderPassDescriptor()rpDesc.colorAttachments[0].texturetexture rpDesc.colorAttachments[0].loadAction.clear rpDesc.colorAttachments[0].storeAction.store rpDesc.colorAttachments[0].clearColorMTLClearColor(red:0,green:0,blue:0,alpha:1)rpDesc.depthAttachment.texturedepthTexture rpDesc.depthAttachment.loadAction.clear rpDesc.depthAttachment.storeAction.dontCare// 不需要保留深度rpDesc.depthAttachment.clearDepth1.0Memoryless TextureMetal 独有的杀手级特性——storageMode .memorylesstexDesc.storageMode.memoryless这种纹理不占用主显存——它只存在于 GPU 的 Tile Memory 中。创建后连主显存空间都不分配。非常适切合于只在 Tile 内使用的 Attachment如临时 Depth/Stencil、G-Buffer 中只在 Lighting Subpass 使用的中间数据。前提该 Attachment 的storeAction必须是.dontCare——因为数据不会被写回显存。渲染letencodercommandBuffer.makeRenderCommandEncoder(descriptor:rpDesc)!// Draw calls...encoder.endEncoding()6.6 WebGPUWebGPU 是 Web 平台的下一代图形 API概念设计受 Vulkan 和 Metal 影响较大。创建纹理consttexturedevice.createTexture({size:[1920,1080],format:rgba8unorm,usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING,});constviewtexture.createView();Render PassconstpassEncodercommandEncoder.beginRenderPass({colorAttachments:[{view:view,clearValue:{r:0,g:0,b:0,a:1},loadOp:clear,storeOp:store,}],depthStencilAttachment:{view:depthView,depthClearValue:1.0,depthLoadOp:clear,depthStoreOp:discard,},});// Draw calls...passEncoder.end();注意 WebGPU 的loadOp/storeOp语义和 Vulkan / Metal 一致。不需要预先创建 Render Pass 对象类似 Vulkan 的 Dynamic Rendering。6.7 横向对比表概念OpenGLDX11DX12VulkanMetalWebGPURT 资源Texture / RenderbufferID3D11Texture2DID3D12ResourceVkImageMTLTextureGPUTextureRT 视图N/A直接挂载RenderTargetViewRTV DescriptorVkImageView隐式Texture 本身GPUTextureView容器FBON/AN/AVkFramebufferMTLRenderPassDescriptorN/ARender Pass无概念无概念BeginRenderPassVkRenderPassMTLRenderCommandEncoderbeginRenderPassloadOp无隐式 Load无手动 ClearBeginRenderPass 参数loadOploadActionloadOpstoreOp无隐式 Store无隐式 StoreEndRenderPass 参数storeOpstoreActionstoreOp状态转换隐式驱动自动隐式驱动自动ResourceBarrierPipeline Barrier / Layout部分隐式隐式MRT 上限通常 8888至少88Depth/StencilFBO AttachmentDepthStencilViewDSV DescriptorAttachmentdepthAttachmentdepthStencilAttachmentMemoryless❌❌❌Lazily Allocated.memoryless❌心智模型总结所有 API 的核心流程都是 1. 创建资源一块带格式的显存 2. 创建作为 RT 的视图 3. 告诉 GPU 接下来画到这儿绑定 4. 声明 loadOp / storeOp现代 API 5. 画东西 6. 状态转换或结束 Render Pass 7. 在后续 Pass 中作为纹理采样术语不同流程相通。本章小结OpenGL最简单但控制力最弱——FBO Attachment驱动管一切DX11Resource View 模型清晰驱动仍管同步和状态DX12完全显式——Descriptor Heap Resource Barrier 手动同步Vulkan最复杂但最灵活——RenderPass/Subpass Pipeline Barrier Dynamic Rendering 简化版Metal设计最优雅——RenderPassDescriptor Memoryless移动端优化杀手WebGPUWeb 平台的Metal 精简版——loadOp/storeOp 隐式同步 思考题为什么 Vulkan 需要VkRenderPassVkFramebuffer两层对象而 Metal 只需要MTLRenderPassDescriptor一层这种设计差异反映了什么理念如果让你设计一个面向 2030 年的图形 API你会保留 Render Pass 的概念吗它的 loadOp/storeOp 是必须显式声明的还是可以由编译器推导OpenGL 的隐式状态机和 Vulkan 的显式 Barrier哪种模型对 GPU 驱动的实现更友好为什么下一章我们聚焦格式和色彩空间——这是决定 RT 精度、范围和带宽的核心参数。选错一个格式轻则浪费一半带宽重则色带(banding)满屏用错一个色彩空间你的后处理将在物理不正确的基础上做所有计算。这些坑比 API 用法本身更隐蔽也更致命。