ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C++游戏开发异步渲染技术:多线程命令、异步计算与GPU驱动渲染实战

2026/8/9 2:12:12 拓冰建站 浏览量
C++游戏开发异步渲染技术:多线程命令、异步计算与GPU驱动渲染实战 1. 项目概述为什么异步渲染是C游戏开发的“秘密武器”在C游戏开发的世界里帧率FPS是衡量游戏流畅度的黄金标准。玩家每一次卡顿、每一次画面撕裂背后往往都是CPU或GPU在某个环节“等”得太久。传统同步渲染就像一条单车道所有车辆渲染指令必须排队通过一旦前面有辆慢车比如一个复杂的物理计算整条路就堵死了。而异步渲染技术就是为这条单车道开辟多条并行辅路甚至建立立交桥让慢车和快车各行其道互不阻塞。这不仅仅是“优化”而是从根本上重构了渲染管线的工作方式。我经历过太多项目初期帧率轻松破百随着玩法复杂度和美术资源增加帧率开始“稳不住”出现间歇性卡顿。排查下来十有八九是主渲染线程被某些耗时操作“绑架”了。这时引入异步渲染技术往往能起到立竿见影的效果。它解决的不仅仅是“快”更是“稳”——保证游戏帧率的稳定性和可预测性这对于竞技类游戏或追求极致体验的3A大作至关重要。本文将深入拆解三种在C游戏开发中经过实战检验的异步渲染技术多线程命令列表提交、异步计算着色器以及GPU驱动的间接渲染。无论你是正在为项目卡顿烦恼的引擎程序员还是希望深入理解现代图形API的渲染爱好者这些内容都将为你提供可直接落地的思路和方案。2. 核心思路拆解从“同步等待”到“并行流水线”传统的即时模式渲染Immediate Mode Rendering是同步的典型代表。你的游戏循环可能是这样的更新逻辑Update- 准备渲染数据Prepare- 提交绘制命令Draw- 等待GPU完成Present/Wait。其中“提交绘制命令”这一步往往在主线程中同步进行CPU需要等待GPU驱动完成命令的转换和排队这个过程如果遇到复杂的状态切换或大量Draw Call就会产生CPU端的空闲等待。异步渲染的核心思想是将“命令录制”与“命令提交”解耦并将原本串行的工作流拆分成可以并行执行的阶段。这背后依赖几个关键技术点2.1 CPU多线程并行录制这是最直接的一层。现代图形API如Vulkan、DirectX 12引入了命令列表Command List的概念。你可以提前在多个线程中录制好绘制命令这些命令列表是独立的。在主线程或专用的提交线程需要渲染时将这些预先录制好的命令列表一次性提交给GPU队列。这样命令录制的耗时就从关键路径上移除了。关键在于每个线程需要管理好自己的命令分配器和相关资源避免线程间竞争。2.2 GPU内部的异步计算GPU并非只有一个“图形流水线”。现代GPU通常包含专为通用并行计算设计的计算单元CU/Stream Processor。异步计算Async Compute允许我们将一些非图形任务如粒子模拟、后期处理、遮挡剔除提交到独立的计算队列中与图形渲染队列同时执行。只要资源依赖处理得当就能让GPU的图形和计算单元同时“忙”起来大幅提升硬件利用率。2.3 GPU驱动的渲染这是更激进的一步旨在减少CPU对渲染流程的干预。通过间接绘制Indirect Drawing和GPU端的数据结构如缓冲计数器让GPU自己决定“画什么”和“画多少”。CPU只需要准备好场景数据和一个间接参数缓冲区GPU通过计算着色器进行视锥剔除、细节层次LOD选择等并填充间接绘制参数然后直接执行绘制。这极大地降低了CPU的每帧负担特别适合拥有大量重复对象如植被、人群的场景。这三种技术并非互斥而是可以层层叠加构建出一个从CPU到GPU的深度异步流水线。选择哪种或组合哪些取决于你的项目瓶颈具体在哪里。3. 技术一多线程命令列表的实战部署DirectX 12和Vulkan都原生支持多线程命令列表。我们以DirectX 12为例因为它有更明确的“命令列表”和“命令队列”抽象便于理解。3.1 基础架构搭建首先你需要创建多个命令列表和对应的命令分配器。一个常见的模式是“每帧每线程”或“池化”模式。// 假设我们有一个渲染帧上下文 FrameContext struct FrameContext { ComPtrID3D12CommandAllocator mainCmdAllocator; ComPtrID3D12GraphicsCommandList mainCmdList; // 为工作线程准备的资源池 std::vectorWorkerThreadCommandResources workerResources; }; struct WorkerThreadCommandResources { ComPtrID3D12CommandAllocator cmdAllocator; ComPtrID3D12GraphicsCommandList cmdList; // 可能还需要该线程专用的描述符堆、临时上传堆等 };在帧开始时重置主线程的命令分配器和列表。对于工作线程的资源可以采用池化管理避免频繁创建销毁。每个工作线程从池中取出一个空闲的WorkerThreadCommandResources进行录制。3.2 工作线程的任务划分如何划分任务给不同线程是关键。通常按渲染对象Render Object或渲染通道Render Pass进行划分。例如线程A录制阴影贴图的绘制命令。线程B录制不透明物体的GBuffer绘制命令。线程C录制天空盒、透明物体等屏幕空间效果的绘制命令。划分原则是任务间资源依赖尽可能少。如果两个任务都需要频繁切换同一套管线状态PSO或绑定同一组资源放在不同线程可能因资源同步反而降低效率。3.3 命令列表的录制与提交工作线程录制的伪代码流程void WorkerThreadRenderFunction(WorkerThreadCommandResources res, const RenderBatch batch) { // 1. 重置本线程的命令列表复用分配器 ThrowIfFailed(res.cmdList-Reset(res.cmdAllocator.Get(), nullptr)); // 2. 设置本线程所需的根签名、视口等全局状态可能与其他线程不同 res.cmdList-SetGraphicsRootSignature(threadRootSig.Get()); res.cmdList-RSSetViewports(1, threadViewport); // 3. 录制具体的绘制命令 for (const auto obj : batch.objects) { res.cmdList-SetPipelineState(obj.pso.Get()); res.cmdList-IASetVertexBuffers(0, 1, obj.vertexBufferView); res.cmdList-DrawInstanced(obj.vertexCount, 1, 0, 0); } // 4. 结束录制 ThrowIfFailed(res.cmdList-Close()); }所有工作线程完成后主渲染线程收集所有已关闭Closed的命令列表并一次性提交到GPU命令队列// 在主线程或提交线程中 std::vectorID3D12CommandList* cmdListsToExecute; cmdListsToExecute.push_back(mainCmdList.Get()); for (auto worker : workerResources) { cmdListsToExecute.push_back(worker.cmdList.Get()); } commandQueue-ExecuteCommandLists(cmdListsToExecute.size(), cmdListsToExecute.data());3.4 关键注意事项与避坑指南注意多线程命令列表最棘手的部分是资源屏障同步。如果你在一个线程中向一个纹理渲染UAV/Render Target然后在另一个线程中将它作为纹理读取SRV必须在提交命令列表前插入正确的资源屏障Resource Barrier。最佳实践是由主线程统一管理所有资源的生命周期和状态转换工作线程只负责录制“使用”资源的命令而不负责改变其状态。主线程在提交前根据本帧的资源使用情况在命令队列开头插入所有必要的屏障。另一个常见坑点是描述符堆Descriptor Heap的线程安全。每个线程最好使用自己独立的描述符堆或者使用锁保护的全局描述符堆。我推荐前者虽然会消耗一些描述符内存但彻底避免了锁竞争对性能更友好。实测下来对于Draw Call数量在5000以上的场景采用3-4个工作线程并行录制命令列表通常能减少20%-40%的CPU渲染线程时间。但线程数并非越多越好超过GPU硬件队列的并发处理能力或引入过多的同步开销收益会递减。4. 技术二异步计算着色器的性能榨取现代GPU如NVIDIA的Maxwell架构之后AMD的GCN架构之后都支持图形队列和计算队列的并行执行。异步计算的核心是让计算任务“见缝插针”利用图形渲染间隙的GPU空闲周期。4.1 识别适合异步计算的任务不是所有计算任务都适合丢给Async Compute。理想的任务特征是计算密集访存相对规整如FFT、模糊、粒子物理更新。与图形渲染管线有天然的“空隙”例如在完成深度预渲染Z-Prepass后不透明物体渲染前有一段间隙可以执行遮挡剔除Occlusion Culling的计算着色器。对延迟不敏感计算结果不需要立即用于当前帧的显示。下一帧才用到的数据预计算是完美候选。一个经典用例是基于计算着色器的后处理链。比如Bloom效果通常需要多次高斯模糊。你可以将第一次下采样和模糊提交到图形队列同时将后续的多次模糊Pass提交到异步计算队列。只要确保第一次模糊的结果资源一张纹理被正确设置屏障计算队列就能在图形队列使用该纹理的间隙开始工作。4.2 DirectX 12中的异步计算实现DX12中你需要创建类型为D3D12_COMMAND_LIST_TYPE_COMPUTE的命令队列和命令列表。// 创建计算队列 D3D12_COMMAND_QUEUE_DESC computeQueueDesc {}; computeQueueDesc.Type D3D12_COMMAND_LIST_TYPE_COMPUTE; computeQueueDesc.Priority D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; ThrowIfFailed(device-CreateCommandQueue(computeQueueDesc, IID_PPV_ARGS(m_computeQueue))); // 创建计算命令列表和分配器类似图形命令列表调度策略是关键。假设我们有一项粒子模拟任务帧N在图形队列渲染帧N的同时将粒子模拟用于帧N1提交到计算队列。帧间同步需要使用围栏Fence来确保帧N的图形任务不会读取还在被计算队列写入的粒子缓冲区。通常让计算队列的围栏值落后图形队列1-2帧。4.3 资源依赖与屏障管理这是异步计算中最容易出错的地方。如果计算着色器和像素着色器要读写同一块资源如UAV必须通过资源屏障明确同步。// 假设我们要在计算着色器中写入一个UAV纹理然后在图形管线的像素着色器中读取它 // 在计算命令列表执行前需要将纹理状态从通用读取转为UAV D3D12_RESOURCE_BARRIER uavBarrierBeforeCompute; uavBarrierBeforeCompute.Type D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; uavBarrierBeforeCompute.Transition.pResource particleTexture.Get(); uavBarrierBeforeCompute.Transition.StateBefore D3D12_RESOURCE_STATE_COMMON; uavBarrierBeforeCompute.Transition.StateAfter D3D12_RESOURCE_STATE_UNORDERED_ACCESS; computeCmdList-ResourceBarrier(1, uavBarrierBeforeCompute); // ... 执行计算着色器 ... // 在计算命令列表执行后或图形命令列表使用该纹理前需要将纹理状态从UAV转为SRV D3D12_RESOURCE_BARRIER srvBarrierAfterCompute; srvBarrierAfterCompute.Type D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; srvBarrierAfterCompute.Transition.pResource particleTexture.Get(); srvBarrierAfterCompute.Transition.StateBefore D3D12_RESOURCE_STATE_UNORDERED_ACCESS; srvBarrierAfterCompute.Transition.StateAfter D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; // 这个屏障可以放在计算命令列表的末尾也可以放在图形命令列表的开头 graphicsCmdList-ResourceBarrier(1, srvBarrierAfterCompute);更复杂的情况涉及多个队列间的资源竞争。DX12提供了D3D12_RESOURCE_BARRIER_TYPE_ALIASING和D3D12_RESOURCE_BARRIER_TYPE_UAV来处理这些高级同步场景。我的经验是初期尽量简化资源依赖让异步计算任务使用独立的资源避免共享可以大幅降低同步复杂度。4.4 性能收益与权衡正确使用异步计算通常能带来5%-15%的整体帧时间提升。这个数字看起来不大但在已经高度优化的渲染器中每一毫秒都至关重要。代价是显著的复杂性提升调试困难GPU Hang或驱动超时更难定位资源状态管理如履薄冰。建议项目中期当图形管线基本稳定后再逐步引入异步计算并且从最独立、最计算密集的任务开始。5. 技术三GPU驱动渲染与间接绘制的深度应用当你的场景有数万棵草、数万个碎石时即使使用了实例化InstancingCPU遍历所有对象、设置常量缓冲区、发起Draw Call的开销依然巨大。GPU驱动渲染将这部分决策权下放给GPU。5.1 间接绘制的原理间接绘制的核心是DrawIndexedInstancedIndirect或DrawInstancedIndirect这类API。它们不从CPU接收绘制参数而是从一个GPU缓冲区称为间接参数缓冲区中读取参数。这个缓冲区的内容可以由CPU填充但更酷的方式是由GPU的计算着色器来填充。流程如下CPU将场景中所有对象的包围盒、LOD信息等数据上传到GPU缓冲区一次上传或动态更新。CPU发起一个计算着色器调度。这个计算着色器对每个对象执行视锥剔除、遮挡查询基于上一帧的深度缓冲、LOD选择等测试。计算着色器根据测试结果将需要渲染的对象的绘制参数索引数、实例数、起始索引位置等追加到一个间接参数缓冲区中。同时它还会维护一个原子计数器记录实际需要绘制的对象数量。CPU调用DrawIndexedInstancedIndirect并传入包含原子计数器的缓冲区地址。GPU会读取这个计数器然后从间接参数缓冲区中取出相应数量的绘制命令并执行。5.2 实现一个简单的GPU剔除系统首先定义数据结构// CPU端对象数据 struct ObjectData { DirectX::XMFLOAT4X4 worldMatrix; DirectX::XMFLOAT3 boundingSphereCenter; float boundingSphereRadius; // ... 其他如材质ID、LOD索引等 }; // GPU端计算着色器输入 StructuredBufferObjectData g_objectData : register(t0); RWStructuredBufferDrawIndexedArgs g_indirectDrawArgs : register(u0); // 间接参数缓冲区 RWByteAddressBuffer g_drawCount : register(u1); // 原子计数器缓冲区 // DrawIndexedArgs 结构需要匹配 D3D12_DRAW_INDEXED_ARGUMENTS struct DrawIndexedArgs { uint IndexCountPerInstance; uint InstanceCount; uint StartIndexLocation; int BaseVertexLocation; uint StartInstanceLocation; };计算着色器HLSL核心逻辑[numthreads(256, 1, 1)] void CullAndAppendCS(uint3 dispatchThreadID : SV_DispatchThreadID) { uint objectIndex dispatchThreadID.x; if (objectIndex g_totalObjectCount) return; ObjectData obj g_objectData[objectIndex]; // 执行视锥剔除这里简化实际需要将包围球变换到视锥空间 bool isVisible /* 视锥剔除逻辑 */; // 可选执行遮挡剔除采样上一帧的Hi-Z Buffer // bool isOccluded /* 遮挡查询逻辑 */; if (isVisible /* !isOccluded */) { // 使用原子操作获取一个追加位置 uint drawIndex; g_drawCount.InterlockedAdd(0, 1, drawIndex); // 0是字节偏移量 // 填充绘制参数 DrawIndexedArgs args; args.IndexCountPerInstance GetIndexCountForLOD(obj.lod); args.InstanceCount 1; // 如果是实例化这里可能是1 args.StartIndexLocation GetStartIndexForLOD(obj.lod); args.BaseVertexLocation 0; args.StartInstanceLocation objectIndex; // 可以用作实例ID在顶点着色器中读取 per-object 数据 g_indirectDrawArgs[drawIndex] args; } }CPU端在渲染循环中// 1. 绑定计算着色器资源并调度计算着色器线程组数量 ceil(对象总数 / 256) // 2. 在图形命令列表中设置好顶点/索引缓冲区、管线状态等 // 3. 执行间接绘制 commandList-ExecuteIndirect( pCommandSignature, // 需要预先创建的命令签名描述了间接参数的布局 MAX_POSSIBLE_DRAWS, // 最大可能绘制次数防止缓冲区溢出 pIndirectArgumentBuffer, // 包含原子计数器的缓冲区 counterOffset, // 计数器在缓冲区中的偏移 pArgumentBuffer, // 间接参数缓冲区 argsOffset // 参数起始偏移 );5.3 高级优化批处理与合并基础的GPU剔除为每个可见对象生成一个独立的Draw Call参数这仍然可能产生成千上万个间接Draw Call存在一定的驱动开销。更高级的优化是批处理合并材质批处理在计算着色器中不仅进行剔除还将使用相同材质、相同顶点缓冲区的对象进行合并。输出时不是为每个对象生成一个DrawIndexedArgs而是为每个材质批次生成一个并设置正确的InstanceCount。这需要更复杂的数据结构和排序。Meshlet渲染这是DX12 Ultimate引入的Mesh Shader管线的一部分。它将网格细分为更小的Meshlet在GPU上进行精细的剔除和LOD选择可以做到像素级别的剔除效率极高是GPU驱动渲染的终极形态之一。5.4 适用场景与限制GPU驱动渲染在植被渲染、大规模粒子系统、海量建筑/碎石等场景下效果拔群能将CPU从数万个对象的遍历和提交中解放出来。但它也有局限启动延迟需要先运行计算着色器才能进行绘制引入了固定的GPU计算开销。对于小场景可能得不偿失。调试地狱GPU填充的缓冲区如果出错会导致完全错误的绘制或直接驱动超时调试信息极其有限。动态对象处理对于每帧位置、状态都变化的对象CPU每帧更新其GPU数据包围盒、矩阵的开销需要权衡。我的建议是将其用于相对静态或规律运动如随风摇摆的草的大量重复物体。对于主角、NPC等动态物体仍采用传统的CPU驱动渲染。6. 组合拳实战构建一个混合异步渲染管线在实际项目中我们很少单独使用某一项技术而是将它们组合起来形成一个完整的异步渲染架构。这里我分享一个在中等规模开放世界项目中使用的管线设计它融合了上述三种技术。6.1 帧生命周期设计我们将一帧分为多个阶段每个阶段在不同线程或GPU队列上并行执行阶段0异步计算队列与上一帧图形队列并行执行粒子物理模拟为当前帧做准备、异步纹理加载后的Mipmap生成。阶段1主线程处理输入、更新游戏逻辑、收集渲染命令。此时工作线程已经开始并行录制阴影贴图、深度预渲染等命令列表。阶段2主线程/提交线程等待工作线程命令列表录制完成。执行资源屏障转换。将图形命令列表和计算命令列表分别提交到图形队列和计算队列。阶段3GPU端图形队列和计算队列并行执行。图形队列执行渲染计算队列执行后处理链中的非关键路径计算如运动模糊的向量计算、下一帧的遮挡深度图生成。这个流程的关键是帧流水线化。阶段0的计算任务是为当前帧服务的但它利用的是上一帧时GPU的空闲时间。这需要精细的围栏同步来保证数据就绪。6.2 资源同步策略我们建立了一个中心化的“资源追踪器”Resource Tracker。任何线程或队列想要使用资源都必须先声明其需要的状态如D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE。资源追踪器会记录资源当前状态和未来状态并在提交点自动插入所有必要的屏障。class ResourceStateTracker { std::unordered_mapID3D12Resource*, D3D12_RESOURCE_STATES m_globalState; std::vectorD3D12_RESOURCE_BARRIER m_pendingBarriers; public: void ResourceBarrierIfNeeded(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* resource, D3D12_RESOURCE_STATES newState) { D3D12_RESOURCE_STATES oldState m_globalState[resource]; if (oldState ! newState) { D3D12_RESOURCE_BARRIER barrier CD3DX12_RESOURCE_BARRIER::Transition( resource, oldState, newState); m_pendingBarriers.push_back(barrier); m_globalState[resource] newState; } } void FlushBarriers(ID3D12GraphicsCommandList* cmdList) { if (!m_pendingBarriers.empty()) { cmdList-ResourceBarrier(m_pendingBarriers.size(), m_pendingBarriers.data()); m_pendingBarriers.clear(); } } };工作线程在录制命令时不直接调用ResourceBarrier而是调用ResourceBarrierIfNeeded来记录状态变更需求。主线程在提交所有命令列表前调用FlushBarriers在命令队列开头统一插入屏障。这确保了跨线程、跨队列的资源状态一致性。6.3 性能分析与调试工具异步渲染让性能分析变得复杂。传统的单线程CPU Profiler可能不再够用。必须依赖更强大的工具Intel GPA, NVIDIA Nsight Graphics, RenderDoc这些帧调试器可以捕获一帧内所有GPU队列的活动可视化地展示图形队列和计算队列的执行时间线帮助你发现队列间的空闲或竞争。自定义GPU时间戳查询在命令列表中插入时间戳测量每个渲染阶段、每个计算着色器的具体执行时间。这对于优化异步计算的任务划分至关重要。CPU端的线程性能分析使用std::chrono或平台特定高精度计时器测量工作线程命令录制的耗时确保负载均衡。一个常见的性能反模式是“虚假的并行”。比如你开了4个线程录制命令但其中一个线程的任务量是其他的三倍导致主线程必须等待它完成其他线程早早就空闲了。使用负载均衡算法或动态任务分配如任务窃取可以缓解这个问题。7. 常见陷阱、调试技巧与进阶思考即使理解了原理在实际编码中依然会踩很多坑。这里记录几个让我调试到深夜的典型问题及其解决方法。7.1 多线程命令列表的“幽灵”错误症状渲染结果随机错误有时正常有时出现错位或花屏且错误不具重现性。 排查这极大概率是资源生命周期问题。你在线程A的命令列表中引用了一个资源如纹理但这个资源可能在线程B的命令列表录制完成前就被释放或覆盖了。记住命令列表只是记录了命令真正执行是在提交之后。你必须确保命令列表引用的所有资源在其被GPU执行完毕前通过围栏信号都保持有效。 解决为每个帧Frame-in-Flight维护独立的资源集合如常量缓冲区环。使用引用计数或智能指针管理跨帧资源。对于临时上传的资源使用帧分配器Frame Allocator管理确保其在帧结束后才回收。7.2 异步计算导致的画面撕裂或闪烁症状使用异步计算进行后处理如Bloom时画面偶尔出现一闪而过的错误颜色或撕裂。 排查这几乎是资源屏障缺失或错误的典型标志。计算着色器写入的UAV纹理在像素着色器读取之前没有正确地从D3D12_RESOURCE_STATE_UNORDERED_ACCESS状态转换到D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE状态。或者屏障插入的位置不对比如插在了计算命令列表执行之后但图形命令列表在另一个队列屏障未被那个队列感知。 解决仔细绘制资源在帧内的状态迁移图。使用图形调试器如Nsight的“资源状态”视图检查每一帧中关键资源的状态变化是否符合预期。养成在队列间传递资源时显式插入屏障的习惯。7.3 GPU驱动渲染的驱动超时TDR症状运行GPU剔除计算着色器后屏幕卡住然后恢复驱动重置。 排查首先检查计算着色器是否有越界访问。例如你的原子计数器初始值为0但InterlockedAdd后得到的drawIndex可能超过了间接参数缓冲区的分配大小。其次检查线程组调度数量是否正确。numthreads(256,1,1)和Dispatch(对象总数/256, 1, 1)如果对象总数不是256的整数倍需要向上取整否则最后一部分线程不会执行可能导致逻辑错误。 解决在计算着色器中添加边界检查if (objectIndex totalCount) return;。在CPU端确保间接参数缓冲区足够大通常按最大可能对象数分配。使用StructuredBuffer或ByteAddressBuffer的IncrementCounter方法可能比手动原子操作更安全。7.4 进阶思考何时不该用异步渲染异步渲染不是银弹它增加了架构的复杂度和维护成本。在以下情况你可能需要慎重考虑小型或2D游戏渲染压力很小同步渲染简单可靠引入多线程和异步带来的收益微乎其微却增加了bug风险。团队经验不足如果团队对图形API和多线程编程不熟悉强行上马异步渲染会导致项目进度失控。先从优化Draw Call、合并批次、使用实例化这些同步优化手段开始。引擎或中间件限制如果你使用的是未充分暴露底层图形API的引擎可能无法实现精细的异步控制。我的个人经验是异步渲染是应对性能瓶颈的“高级武器库”。在项目初期确立一个清晰的、支持异步扩展的渲染架构是值得的但具体的异步化优化可以放在性能瓶颈真正出现时再进行。永远遵循“先测量后优化”的原则用性能分析工具定位到真正的热点再决定用哪一把“秘密武器”去攻克它。