From c491ab0c65cdf7ed1b7bec2d1701e52cd54b427f Mon Sep 17 00:00:00 2001 From: "LAPTOP-BBDAGDBG\\Ray" <2210906181@qq.com> Date: Thu, 2 Apr 2026 18:43:35 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20scene=20culling=20light=E9=80=A0?= =?UTF-8?q?=E6=88=90UBO=E6=97=A0=E6=B3=95=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cocos/rendering/custom/scene-culling.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cocos/rendering/custom/scene-culling.ts b/cocos/rendering/custom/scene-culling.ts index 127074b8862..9d93284ff8f 100644 --- a/cocos/rendering/custom/scene-culling.ts +++ b/cocos/rendering/custom/scene-culling.ts @@ -821,7 +821,8 @@ export class LightResource { // Already added const existingLightID = this.lightIndex.get(light); if (existingLightID !== undefined) { - return existingLightID; + return existingLightID * this.elementSize; + // return existingLightID; } if (!this.lightBuffer) { From aacf2838d990f6544a63ad40d189fd56267c5ee5 Mon Sep 17 00:00:00 2001 From: "LAPTOP-BBDAGDBG\\Ray" <2210906181@qq.com> Date: Sat, 11 Apr 2026 02:27:48 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20=E7=BC=93=E5=AD=98=E7=9A=84=20BlitDe?= =?UTF-8?q?sc.=5Fblit=20=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cocos/rendering/custom/executor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/cocos/rendering/custom/executor.ts b/cocos/rendering/custom/executor.ts index ffcbfd764f4..44092063420 100644 --- a/cocos/rendering/custom/executor.ts +++ b/cocos/rendering/custom/executor.ts @@ -619,6 +619,7 @@ class DeviceRenderQueue implements RecordingInterface { if (!this._blitDesc) { this._blitDesc = new BlitDesc(blit); } + this._blitDesc.blit = blit; this._blitDesc.createScreenQuad(); this._blitDesc.createStageDescriptor(); } From a92d9141590c2731bbccb2e111f1a356e4281ab6 Mon Sep 17 00:00:00 2001 From: "LAPTOP-BBDAGDBG\\Ray" <2210906181@qq.com> Date: Mon, 13 Apr 2026 22:05:30 +0800 Subject: [PATCH 3/6] fix: pass merge --- cocos/rendering/custom/compiler.ts | 2 +- cocos/rendering/custom/define.ts | 28 +++-- cocos/rendering/custom/executor.ts | 142 +++++++++++++++++++++---- cocos/rendering/custom/web-pipeline.ts | 16 +-- 4 files changed, 154 insertions(+), 34 deletions(-) diff --git a/cocos/rendering/custom/compiler.ts b/cocos/rendering/custom/compiler.ts index 225ec4252ca..9e00044af9e 100644 --- a/cocos/rendering/custom/compiler.ts +++ b/cocos/rendering/custom/compiler.ts @@ -165,7 +165,7 @@ class PassVisitor implements RenderGraphVisitor { } } if (validPass) return; - if (rg.getValid(this.sceneID)) { + if (rg.getValid(this.passID)) { for (const [readName, raster] of pass.rasterViews) { context.pipeline.resourceUses.push(readName); } diff --git a/cocos/rendering/custom/define.ts b/cocos/rendering/custom/define.ts index c5f856b808a..ef30debfb06 100644 --- a/cocos/rendering/custom/define.ts +++ b/cocos/rendering/custom/define.ts @@ -702,6 +702,7 @@ export function AlignUp (value: number, alignment: number): number { return (value + (alignment - 1)) & ~(alignment - 1); } const kLightMeterScale = 10000; +const _lightUboVec4 = new Float32Array(4); export function SetLightUBO ( light: Light | null, bHDR: boolean, @@ -711,7 +712,7 @@ export function SetLightUBO ( offset: number, elemSize: number, ): void { - const vec4Array = new Float32Array(4); + const vec4Array = _lightUboVec4; let size = 0.0; let range = 0.0; let luminanceHDR = 0.0; @@ -763,20 +764,28 @@ export function SetLightUBO ( buffer.set(vec4Array, index); index = offset + UBOForwardLightEnum.LIGHT_SIZE_RANGE_ANGLE_OFFSET; - vec4Array.set([size, range, 0, 0]); + vec4Array[0] = size; + vec4Array[1] = range; + vec4Array[2] = 0; + vec4Array[3] = 0; buffer.set(vec4Array, index); index = offset + UBOForwardLightEnum.LIGHT_COLOR_OFFSET; - const color = light ? light.color : new Color(); if (light && light.useColorTemperature) { + const color = light.color; const tempRGB = light.colorTemperatureRGB; buffer[index++] = color.x * tempRGB.x; buffer[index++] = color.y * tempRGB.y; buffer[index++] = color.z * tempRGB.z; - } else { + } else if (light) { + const color = light.color; buffer[index++] = color.x; buffer[index++] = color.y; buffer[index++] = color.z; + } else { + buffer[index++] = 0; + buffer[index++] = 0; + buffer[index++] = 0; } if (bHDR) { @@ -879,7 +888,7 @@ export class RenderPassMergeInfo { } const passOrders: RasterPass[] = []; -const rpCombineMap: Map = new Map(); +export const rpCombineMap: Map = new Map(); export const rpMergeInfos: Map = new Map(); const rpMergeInfoPool = new RecyclePool((): RenderPassMergeInfo => new RenderPassMergeInfo(), 16); export function resetPassMGState (): void { @@ -888,6 +897,14 @@ export function resetPassMGState (): void { rpCombineMap.clear(); passOrders.length = 0; } +export function rebuildPassMGInfo (orderedPasses: Readonly): void { + rpMergeInfoPool.reset(); + rpMergeInfos.clear(); + passOrders.length = 0; + for (const pass of orderedPasses) { + processPassMG(pass); + } +} export function processPassMG (pass: RasterPass): void { const currCHash = rpCombineMap.get(pass)!; const currRPInfo = rpMergeInfoPool.add(); @@ -960,5 +977,4 @@ export function genHashValue (pass: RasterPass): void { pass.hashValue = hashCombineStr(hashCodeParts.join('')); rpCombineMap.set(pass, hashCombineStr(combineHashParts.join(''))); - processPassMG(pass); } diff --git a/cocos/rendering/custom/executor.ts b/cocos/rendering/custom/executor.ts index 44092063420..929b353fa8d 100644 --- a/cocos/rendering/custom/executor.ts +++ b/cocos/rendering/custom/executor.ts @@ -137,8 +137,7 @@ import { bool, getDescriptorSetDataFromLayout, getRenderArea, - RenderPassMergeInfo, - rpMergeInfos, + rpCombineMap, updateGlobalDescBinding, } from './define'; import { LightResource, SceneCulling } from './scene-culling'; @@ -462,7 +461,7 @@ class BlitDesc { // Set the count of lights in cc_lightDir[0].w const offset = fieldLen * 3 + 3; - this._lightBufferData.set([idx], offset); + this._lightBufferData[offset] = idx; cmdBuff.updateBuffer(this._lightVolumeBuffer!, this._lightBufferData); } @@ -609,7 +608,15 @@ class DeviceRenderQueue implements RecordingInterface { this.queueHint = renderQueue.hint; const viewport = this._viewport = renderQueue.viewport; if (viewport) { - this._scissor = new Rect(viewport.left, viewport.top, viewport.width, viewport.height); + if (!this._scissor) { + this._scissor = new Rect(); + } + this._scissor.x = viewport.left; + this._scissor.y = viewport.top; + this._scissor.width = viewport.width; + this._scissor.height = viewport.height; + } else { + this._scissor = null; } this.queueId = id; this._devicePass = devicePass; @@ -635,6 +642,8 @@ class DeviceRenderQueue implements RecordingInterface { this._isUpdateUBO = false; this._isUploadInstance = false; this._isUploadBatched = false; + this._viewport = null; + this._scissor = null; this._blitDesc?.reset(); } get graphQueue (): RenderQueue { return this._graphQueue; } @@ -754,6 +763,7 @@ const profilerViewport = new Viewport(); const renderPassArea = new Rect(); const resourceVisitor = new ResourceVisitor(); const textureBlit = new TextureBlit(); +const dispatchInfo = new DispatchInfo(); class DeviceRenderPass implements RecordingInterface { protected _renderPass: RenderPass; protected _framebuffer!: Framebuffer; @@ -893,7 +903,8 @@ class DeviceRenderPass implements RecordingInterface { swapchain ? swapchain.depthStencilTexture : depthTex, ); } - get passMergeInfo (): RenderPassMergeInfo { return rpMergeInfos.get(this._rasterPass)!; } + _needBeginRP = true; + _needEndRP = true; get indexOfRD (): number { return this._idxOfRenderData; } get rasterID (): number { return this._rasterID; } get layoutName (): string { return this._layoutName; } @@ -959,7 +970,7 @@ class DeviceRenderPass implements RecordingInterface { } } beginPass (): void { - if (!this.passMergeInfo.needBeginRP) { + if (!this._needBeginRP) { this.bindGlobalDesc(); return; } @@ -988,7 +999,7 @@ class DeviceRenderPass implements RecordingInterface { } endPass (): void { - if (!this.passMergeInfo.needEndRP) return; + if (!this._needEndRP) return; const cmdBuff = context.commandBuffer; cmdBuff.endRenderPass(); } @@ -1467,7 +1478,11 @@ class BlitInfo { this._width = context.width; this._height = context.height; this._pipelineIAData = this._createQuadInputAssembler(); - const vb = this._genQuadVertexData(SurfaceTransform.IDENTITY, new Rect(0, 0, context.width, context.height)); + quadRect.x = 0; + quadRect.y = 0; + quadRect.width = context.width; + quadRect.height = context.height; + const vb = this._genQuadVertexData(SurfaceTransform.IDENTITY, quadRect); this._pipelineIAData.quadVB!.update(vb); this._createLightVolumes(); const size: number = UBOLocalEnum.SIZE; @@ -1515,9 +1530,23 @@ class BlitInfo { if (this._context.root.device.capabilities.screenSpaceSignY > 0) { [minY, maxY] = [maxY, minY]; } - const vbData = new Float32Array(16); const fillVertices = (x1, y1, u1, v1, x2, y2, u2, v2, x3, y3, u3, v3, x4, y4, u4, v4): void => { - vbData.set([x1, y1, u1, v1, x2, y2, u2, v2, x3, y3, u3, v3, x4, y4, u4, v4]); + vbData[0] = x1; + vbData[1] = y1; + vbData[2] = u1; + vbData[3] = v1; + vbData[4] = x2; + vbData[5] = y2; + vbData[6] = u2; + vbData[7] = v2; + vbData[8] = x3; + vbData[9] = y3; + vbData[10] = u3; + vbData[11] = v3; + vbData[12] = x4; + vbData[13] = y4; + vbData[14] = u4; + vbData[15] = v4; }; switch (surfaceTransform) { case SurfaceTransform.IDENTITY: @@ -1661,6 +1690,9 @@ class ExecutorContext { } export class Executor { + private _resourceUseSet: Set = new Set(); + private _deletes: string[] = []; + private _deletesBuff: string[] = []; constructor ( pipeline: BasicPipeline, device: Device, @@ -1687,12 +1719,19 @@ export class Executor { private _removeDeviceResource (): void { const pipeline: any = context.pipeline; const resourceUses = pipeline.resourceUses; - const deletes: string[] = []; + const resourceUseSet = this._resourceUseSet; + resourceUseSet.clear(); + for (const name of resourceUses) { + resourceUseSet.add(name); + } + + const deletes = this._deletes; + deletes.length = 0; const deviceTexs = context.deviceTextures; - for (const [name, dTex] of deviceTexs) { + for (const [name] of deviceTexs) { const resId = context.resourceGraph.vertex(name); const trait = context.resourceGraph.getTraits(resId); - if (!resourceUses.includes(name)) { + if (!resourceUseSet.has(name)) { switch (trait.residency) { case ResourceResidency.MANAGED: deletes.push(name); @@ -1706,12 +1745,13 @@ export class Executor { deviceTexs.delete(name); } - const deletesBuff: string[] = []; + const deletesBuff = this._deletesBuff; + deletesBuff.length = 0; const deviceBuffs = context.deviceBuffers; - for (const [name, dBuff] of deviceBuffs) { + for (const [name] of deviceBuffs) { const resId = context.resourceGraph.vertex(name); const trait = context.resourceGraph.getTraits(resId); - if (!resourceUses.includes(name)) { + if (!resourceUseSet.has(name)) { switch (trait.residency) { case ResourceResidency.MANAGED: deletesBuff.push(name); @@ -1740,7 +1780,9 @@ export class Executor { context.lightResource.tryUpdateRenderSceneLocalDescriptorSet(context.culling); culling.uploadInstancing(cmdBuff); if (!this._visitor) this._visitor = new RenderVisitor(); + this._visitor.reset(); depthFirstSearch(this._visitor.graphView, this._visitor, this._visitor.colorMap); + this._visitor.flush(); cmdBuff.end(); context.device.queue.submit([cmdBuff]); } @@ -1929,14 +1971,33 @@ class PreRenderVisitor extends BaseRenderVisitor implements RenderGraphVisitor { const gx = value.threadGroupCountX; const gy = value.threadGroupCountY; const gz = value.threadGroupCountZ; - (cmdBuff as any).dispatch(new DispatchInfo(gx, gy, gz)); + dispatchInfo.groupCountX = gx; + dispatchInfo.groupCountY = gy; + dispatchInfo.groupCountZ = gz; + dispatchInfo.indirectBuffer = null; + dispatchInfo.indirectOffset = 0; + (cmdBuff as any).dispatch(dispatchInfo); } } class PostRenderVisitor extends BaseRenderVisitor implements RenderGraphVisitor { + private _pendingEndPass: DeviceRenderPass | null = null; + private _lastCombineHash: number = 0; constructor () { super(); } + reset (): void { + this._pendingEndPass = null; + this._lastCombineHash = 0; + } + flush (): void { + if (this._pendingEndPass) { + this._pendingEndPass._needEndRP = true; + this._pendingEndPass.endPass(); + this._pendingEndPass.postRecord(); + this._pendingEndPass = null; + } + } clear (value: ClearView[]): void { // do nothing } @@ -1950,8 +2011,43 @@ class PostRenderVisitor extends BaseRenderVisitor implements RenderGraphVisitor if (!currPass) return; this.currPass = currPass; context.passShowStatistics = pass.showStatistics; - this.currPass.record(); - this.currPass.postRecord(); + + const currCHash = rpCombineMap.get(pass) ?? 0; + let canMerge = false; + if (this._pendingEndPass && currCHash === this._lastCombineHash) { + for (const [_, raster] of pass.rasterViews) { + if (raster.loadOp === LoadOp.LOAD) { + canMerge = true; + break; + } + } + } + + if (this._pendingEndPass) { + if (canMerge) { + // Skip endRenderPass for previous pass (merge) + this._pendingEndPass._needEndRP = false; + this._pendingEndPass.endPass(); + this._pendingEndPass.postRecord(); + } else { + // End previous pass normally + this._pendingEndPass._needEndRP = true; + this._pendingEndPass.endPass(); + this._pendingEndPass.postRecord(); + } + } + + // Begin and record queues for current pass + currPass._needBeginRP = !canMerge; + currPass._needEndRP = true; + currPass.beginPass(); + for (const queue of currPass.deviceQueues.values()) { + queue.record(); + } + + // Defer endPass to see if next pass can merge + this._pendingEndPass = currPass; + this._lastCombineHash = currCHash; } rasterSubpass (value: RasterSubpass): void { // do nothing @@ -2001,6 +2097,14 @@ export class RenderVisitor extends DefaultVisitor { this._colorMap = new VectorGraphColorMap(context.renderGraph.nv()); } + reset (): void { + this._postVisitor.reset(); + } + + flush (): void { + this._postVisitor.flush(); + } + get graphView (): ReferenceGraphView { return this._graphView; } get colorMap (): VectorGraphColorMap { return this._colorMap; } discoverVertex (u: number, gv: ReferenceGraphView): void { diff --git a/cocos/rendering/custom/web-pipeline.ts b/cocos/rendering/custom/web-pipeline.ts index 999ff4bfc0c..09d8a139134 100644 --- a/cocos/rendering/custom/web-pipeline.ts +++ b/cocos/rendering/custom/web-pipeline.ts @@ -1645,18 +1645,18 @@ export class WebPipeline extends WebSetter implements BasicPipeline { throw new Error('RenderGraph cannot be built without being created'); } resetPassMGState(); - if (DEBUG) { + // if (DEBUG) { if (!this._compiler) { this._compiler = new Compiler(this, this._renderGraph, this._resourceGraph, this._lg); } this._compiler.compile(this._renderGraph); - } else { - this._renderGraph.x.forEach((vert) => { - if (vert.t === RenderGraphValue.RasterPass) { - genHashValue(vert.j as RasterPass); - } - }); - } + // } else { + // this._renderGraph.x.forEach((vert, vertID) => { + // if (vert.t === RenderGraphValue.RasterPass && this._renderGraph!.getValid(vertID)) { + // genHashValue(vert.j as RasterPass); + // } + // }); + // } } execute (): void { From 042c63ae0279547b72946a95c42edcf467d9f378 Mon Sep 17 00:00:00 2001 From: "LAPTOP-BBDAGDBG\\Ray" <2210906181@qq.com> Date: Tue, 14 Apr 2026 23:57:05 +0800 Subject: [PATCH 4/6] =?UTF-8?q?opti:=20terrain=E6=80=A7=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cocos/terrain/terrain.ts | 178 ++++++++++++++++++++++++++++++--------- 1 file changed, 139 insertions(+), 39 deletions(-) diff --git a/cocos/terrain/terrain.ts b/cocos/terrain/terrain.ts index 167f47a2b18..56d0c39d179 100644 --- a/cocos/terrain/terrain.ts +++ b/cocos/terrain/terrain.ts @@ -411,6 +411,13 @@ export class TerrainBlock { private _LevelDistances: number[] = [TERRAIN_LOD_MAX_DISTANCE, TERRAIN_LOD_MAX_DISTANCE, TERRAIN_LOD_MAX_DISTANCE, TERRAIN_LOD_MAX_DISTANCE]; private _bbMin = v3(); private _bbMax = v3(); + private _propsDirty = true; + private _uvScale: Vec4 = new Vec4(1, 1, 1, 1); + private _roughness: Vec4 = new Vec4(1, 1, 1, 1); + private _metallic: Vec4 = new Vec4(0, 0, 0, 0); + private _bbMinWorld = v3(); + private _bbMaxWorld = v3(); + private _tempLodKey: TerrainLodKey = new TerrainLodKey(); constructor (t: Terrain, i: number, j: number) { this._terrain = t; @@ -507,6 +514,11 @@ export class TerrainBlock { this._renderable._updateLightingmap(this.lightmap, this.lightmapUVParam); } + if (!this._propsDirty) { + return; + } + this._propsDirty = false; + const useNormalMap = this._terrain.useNormalMap; const usePBR = this._terrain.usePBR; @@ -526,14 +538,18 @@ export class TerrainBlock { const mtl = this._renderable._currentMaterial; if (mtl !== null) { + const layers = this.layers; const nLayers = this.getMaxLayer(); - const uvScale = new Vec4(1, 1, 1, 1); - const roughness = new Vec4(1, 1, 1, 1); - const metallic = new Vec4(0, 0, 0, 0); + const uvScale = this._uvScale; + const roughness = this._roughness; + const metallic = this._metallic; + uvScale.set(1, 1, 1, 1); + roughness.set(1, 1, 1, 1); + metallic.set(0, 0, 0, 0); if (nLayers === 0) { - if (this.layers[0] !== -1) { - const l0 = this._terrain.getLayer(this.layers[0]); + if (layers[0] !== -1) { + const l0 = this._terrain.getLayer(layers[0]); if (l0 !== null) { uvScale.x = 1.0 / l0.tileSize; @@ -552,8 +568,8 @@ export class TerrainBlock { } } } else if (nLayers === 1) { - const l0 = this._terrain.getLayer(this.layers[0]); - const l1 = this._terrain.getLayer(this.layers[1]); + const l0 = this._terrain.getLayer(layers[0]); + const l1 = this._terrain.getLayer(layers[1]); if (l0 !== null) { uvScale.x = 1.0 / l0.tileSize; @@ -574,9 +590,9 @@ export class TerrainBlock { mtl.setProperty('normalMap1', getNormalTex(l1)); } } else if (nLayers === 2) { - const l0 = this._terrain.getLayer(this.layers[0]); - const l1 = this._terrain.getLayer(this.layers[1]); - const l2 = this._terrain.getLayer(this.layers[2]); + const l0 = this._terrain.getLayer(layers[0]); + const l1 = this._terrain.getLayer(layers[1]); + const l2 = this._terrain.getLayer(layers[2]); if (l0 !== null) { uvScale.x = 1.0 / l0.tileSize; @@ -604,10 +620,10 @@ export class TerrainBlock { mtl.setProperty('normalMap2', getNormalTex(l2)); } } else if (nLayers === 3) { - const l0 = this._terrain.getLayer(this.layers[0]); - const l1 = this._terrain.getLayer(this.layers[1]); - const l2 = this._terrain.getLayer(this.layers[2]); - const l3 = this._terrain.getLayer(this.layers[3]); + const l0 = this._terrain.getLayer(layers[0]); + const l1 = this._terrain.getLayer(layers[1]); + const l2 = this._terrain.getLayer(layers[2]); + const l3 = this._terrain.getLayer(layers[3]); if (l0 !== null) { uvScale.x = 1.0 / l0.tileSize; @@ -667,13 +683,13 @@ export class TerrainBlock { * @engineInternal * @mangle */ - public _updateLevel (camPos: Vec3): void { + public _updateLevel (camPos: Vec3, farClip = TERRAIN_LOD_MAX_DISTANCE): void { const terrain = this._terrain; const terrainNode = terrain.node; const maxLevel = TERRAIN_LOD_LEVELS - 1; - const bbMin = v3(); - const bbMax = v3(); + const bbMin = this._bbMinWorld; + const bbMax = this._bbMaxWorld; Vec3.add(bbMin, this._bbMin, terrainNode.worldPosition); Vec3.add(bbMax, this._bbMax, terrainNode.worldPosition); @@ -682,6 +698,13 @@ export class TerrainBlock { const d2 = Vec3.distance(bbMax, camPos); let d = Math.min(d1, d2); + // Block's closest sampled corner is beyond the camera far clip; + // it won't be visible so pin it to the coarsest LOD and skip further work. + if (d > farClip) { + this._lodLevel = maxLevel; + return; + } + d -= terrain.LodBias; this._lodLevel = 0; @@ -855,13 +878,15 @@ export class TerrainBlock { * @zh 获得最大纹理索引 */ public getMaxLayer (): number { - if (this.layers[3] >= 0) { + const i = this._index[0]; + const j = this._index[1]; + if (this._terrain.getBlockLayer(i, j, 3) >= 0) { return 3; } - if (this.layers[2] >= 0) { + if (this._terrain.getBlockLayer(i, j, 2) >= 0) { return 2; } - if (this.layers[1] >= 0) { + if (this._terrain.getBlockLayer(i, j, 1) >= 0) { return 1; } @@ -885,11 +910,13 @@ export class TerrainBlock { } public _invalidMaterial (): void { + this._propsDirty = true; this._renderable._invalidMaterial(); } public _updateMaterial (init: boolean): void { if (this._renderable._updateMaterial(this, init)) { + this._propsDirty = true; // Need set wrap mode clamp to border if (this.lightmap !== null) { this.lightmap.setWrapMode(WrapMode.CLAMP_TO_BORDER, WrapMode.CLAMP_TO_BORDER); @@ -967,13 +994,16 @@ export class TerrainBlock { * @deprecated since v3.5.0, this is an engine private interface that will be removed in the future. */ public _updateLod (): void { - const key = new TerrainLodKey(); + const key = this._tempLodKey; key.level = this._lodLevel; key.north = this._lodLevel; key.south = this._lodLevel; key.west = this._lodLevel; key.east = this._lodLevel; + const blockCountX = this._terrain.blockCount[0]; + const blockCountY = this._terrain.blockCount[1]; + if (this._index[0] > 0) { const n = this.getTerrain().getBlock(this._index[0] - 1, this._index[1]); key.west = n._lodLevel; @@ -982,7 +1012,7 @@ export class TerrainBlock { } } - if (this._index[0] < this._terrain.info.blockCount[0] - 1) { + if (this._index[0] < blockCountX - 1) { const n = this.getTerrain().getBlock(this._index[0] + 1, this._index[1]); key.east = n._lodLevel; if (key.east < this._lodLevel) { @@ -998,7 +1028,7 @@ export class TerrainBlock { } } - if (this._index[1] < this._terrain.info.blockCount[1] - 1) { + if (this._index[1] < blockCountY - 1) { const n = this.getTerrain().getBlock(this._index[0], this._index[1] + 1); key.south = n._lodLevel; if (key.south < this._lodLevel) { @@ -1010,26 +1040,38 @@ export class TerrainBlock { return; } - this._lodKey = key; + this._lodKey.level = key.level; + this._lodKey.north = key.north; + this._lodKey.south = key.south; + this._lodKey.west = key.west; + this._lodKey.east = key.east; this._updateIndexBuffer(); } + /** + * @engineInternal + * Pin this block to the coarsest LOD level. + * Neighbour seam stitching is handled by the later _updateLod pass. + */ + public _pinToMaxLod (): void { + const maxLevel = TERRAIN_LOD_LEVELS - 1; + this._lodLevel = maxLevel; + } + /** * @deprecated since v3.5.0, this is an engine private interface that will be removed in the future. */ public _resetLod (): void { - const key = new TerrainLodKey(); - key.level = 0; - key.north = 0; - key.south = 0; - key.west = 0; - key.east = 0; - - if (this._lodKey.equals(key)) { + if (this._lodKey.level === 0 && this._lodKey.north === 0 && this._lodKey.south === 0 + && this._lodKey.west === 0 && this._lodKey.east === 0) { return; } - this._lodKey = key; + this._lodKey.level = 0; + this._lodKey.north = 0; + this._lodKey.south = 0; + this._lodKey.west = 0; + this._lodKey.east = 0; this._updateIndexBuffer(); } @@ -1064,7 +1106,11 @@ export class TerrainBlock { private _updateLodBuffer (vertices: Float32Array): void { this._lodLevel = 0; - this._lodKey = new TerrainLodKey(); + this._lodKey.level = 0; + this._lodKey.north = 0; + this._lodKey.south = 0; + this._lodKey.west = 0; + this._lodKey.east = 0; this._calcErrorMetrics(vertices); this._calcLevelDistances(vertices); } @@ -1257,6 +1303,9 @@ export class Terrain extends Component { protected _lod: TerrainLod|null = null; protected _sharedIndexBuffer: Buffer|null = null; protected _sharedLodIndexBuffer: Buffer|null = null; + // Per-camera position cache for LOD update threshold; WeakMap avoids retention of + // destroyed camera objects and correctly handles multi-camera scenes. + private _lastCamPosMap: WeakMap = new WeakMap(); constructor () { super(); @@ -1761,10 +1810,60 @@ export class Terrain extends Component { return; } - for (let i = 0; i < this._blocks.length; ++i) { - this._blocks[i]._updateLevel(cam.position); + const camPos = cam.position; + + // Per-camera threshold: skip if this camera has not moved appreciably since + // its last update. Each camera maintains its own cached position so that + // multi-camera setups (e.g. editor + game view) don't interfere with each other. + let lastPos = this._lastCamPosMap.get(cam); + if (lastPos !== undefined) { + const dx = camPos.x - lastPos.x; + const dy = camPos.y - lastPos.y; + const dz = camPos.z - lastPos.z; + if (dx * dx + dy * dy + dz * dz < 0.01) { + return; + } + // lastPos.set(camPos.x, camPos.y, camPos.z); + } else { + // First call for this camera: register and fall through to force an update. + lastPos = v3(); + // this._lastCamPosMap.set(cam, lastPos); + } + for (let [cam, pos] of Object.entries(this._lastCamPosMap)) { + pos.set(camPos.x, camPos.y, camPos.z); + } + + const farClip = cam.farClip; + + // Convert camera world position to terrain local space so we can compute + // which block grid cells are potentially within farClip range. + const terrainPos = this.node.worldPosition; + const localX = camPos.x - terrainPos.x; + const localZ = camPos.z - terrainPos.z; + const blockSize = TERRAIN_BLOCK_TILE_COMPLEXITY * this._tileSize; + // +1 block margin to ensure border blocks are included for seam stitching. + const blockRadius = Math.ceil(farClip / blockSize) + 1; + const centerBX = Math.floor(localX / blockSize); + const centerBZ = Math.floor(localZ / blockSize); + const minBX = Math.max(0, centerBX - blockRadius); + const maxBX = Math.min(this._blockCount[0] - 1, centerBX + blockRadius); + const minBZ = Math.max(0, centerBZ - blockRadius); + const maxBZ = Math.min(this._blockCount[1] - 1, centerBZ + blockRadius); + + for (let j = 0; j < this._blockCount[1]; ++j) { + for (let i = 0; i < this._blockCount[0]; ++i) { + const block = this._blocks[j * this._blockCount[0] + i]; + if (i >= minBX && i <= maxBX && j >= minBZ && j <= maxBZ) { + // Block is potentially visible: full level calculation with farClip. + block._updateLevel(camPos, farClip); + } else { + // Block is outside farClip range: pin to coarsest level in phase 1. + block._pinToMaxLod(); + } + } } + // Phase 2: apply stitching/index updates after all block levels are finalized. for (let i = 0; i < this._blocks.length; ++i) { this._blocks[i]._updateLod(); } @@ -2107,14 +2206,15 @@ export class Terrain extends Component { const by = Math.floor(y / this.weightMapSize); const block = this.getBlock(bx, by); + const wArr = [w.x, w.y, w.z, w.w]; let i = 0; - if (w.y > w[i] && block.getLayer(1) !== -1) { + if (w.y > wArr[i] && block.getLayer(1) !== -1) { i = 1; } - if (w.y > w[i] && block.getLayer(2) !== -1) { + if (w.z > wArr[i] && block.getLayer(2) !== -1) { i = 2; } - if (w.z > w[i] && block.getLayer(3) !== -1) { + if (w.w > wArr[i] && block.getLayer(3) !== -1) { i = 3; } From 224e34002bd20d623e8bdf3a601a8a9bc70f597e Mon Sep 17 00:00:00 2001 From: "LAPTOP-BBDAGDBG\\Ray" <2210906181@qq.com> Date: Wed, 15 Apr 2026 00:05:34 +0800 Subject: [PATCH 5/6] fix: terrain camera bug --- cocos/terrain/terrain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocos/terrain/terrain.ts b/cocos/terrain/terrain.ts index 56d0c39d179..63fe40763eb 100644 --- a/cocos/terrain/terrain.ts +++ b/cocos/terrain/terrain.ts @@ -1806,7 +1806,7 @@ export class Terrain extends Component { if (!this.lodEnable || this._sharedLodIndexBuffer == null) { return; } - if (cam.scene !== this._getRenderScene()) { + if (cam.scene !== this._getRenderScene() || !(cam.visibility & this.node.layer)) { return; } From 54cfb77f8bf77c6a0629d4e01991f122ebdd6fd6 Mon Sep 17 00:00:00 2001 From: "LAPTOP-BBDAGDBG\\Ray" <2210906181@qq.com> Date: Wed, 15 Apr 2026 00:55:38 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20terrain=20LOD=E8=A3=81=E5=89=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cocos/terrain/terrain.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/cocos/terrain/terrain.ts b/cocos/terrain/terrain.ts index 63fe40763eb..b3f5a854fe2 100644 --- a/cocos/terrain/terrain.ts +++ b/cocos/terrain/terrain.ts @@ -1303,9 +1303,7 @@ export class Terrain extends Component { protected _lod: TerrainLod|null = null; protected _sharedIndexBuffer: Buffer|null = null; protected _sharedLodIndexBuffer: Buffer|null = null; - // Per-camera position cache for LOD update threshold; WeakMap avoids retention of - // destroyed camera objects and correctly handles multi-camera scenes. - private _lastCamPosMap: WeakMap = new WeakMap(); + private _lastCamPosMap: Map = new Map(); constructor () { super(); @@ -1815,7 +1813,8 @@ export class Terrain extends Component { // Per-camera threshold: skip if this camera has not moved appreciably since // its last update. Each camera maintains its own cached position so that // multi-camera setups (e.g. editor + game view) don't interfere with each other. - let lastPos = this._lastCamPosMap.get(cam); + const cameraId = cam.cameraId; + let lastPos = this._lastCamPosMap.get(cameraId); if (lastPos !== undefined) { const dx = camPos.x - lastPos.x; const dy = camPos.y - lastPos.y; @@ -1827,9 +1826,9 @@ export class Terrain extends Component { } else { // First call for this camera: register and fall through to force an update. lastPos = v3(); - // this._lastCamPosMap.set(cam, lastPos); + this._lastCamPosMap.set(cameraId, lastPos); } - for (let [cam, pos] of Object.entries(this._lastCamPosMap)) { + for (const [, pos] of this._lastCamPosMap) { pos.set(camPos.x, camPos.y, camPos.z); } @@ -1864,8 +1863,13 @@ export class Terrain extends Component { } // Phase 2: apply stitching/index updates after all block levels are finalized. - for (let i = 0; i < this._blocks.length; ++i) { - this._blocks[i]._updateLod(); + for (let j = 0; j < this._blockCount[1]; ++j) { + for (let i = 0; i < this._blockCount[0]; ++i) { + if (i >= minBX && i <= maxBX && j >= minBZ && j <= maxBZ) { + const block = this._blocks[j * this._blockCount[0] + i]; + block._updateLod(); + } + } } }