客户端渲染与光照

基本信息

属性 值
客户端入口 client/HEClient.java(@SidedProxy 的 clientSide)
渲染器 client/renderer/HEWaterRenderer.java(注册为 IBlockRenderer)
光照管理 client/light/HELightManager.java + HELightSMPHooks.java
自定义着色器 assets/hydroenergy/shader/hewater/(.vsh / .fsh / .gsh)
客户端 Mixin 9 个 early + 1 个 late(GT 污染渲染器)
相关配置 useLimitedRendering、forceOpenGL、minLightUpdateTimePerSubChunk

渲染能力分档

本 mod 的水体渲染有两档实现,由 useLimitedRendering 切换:

渲染路径 条件 用到的 GL 能力
完整(含自定义着色器) GLContext.getCapabilities().OpenGL32 && !HEConfig.useLimitedRendering OpenGL 3.2
受限 否则 降级路径

三处判定(彼此不完全一致):

位置 条件 用途
client/renderer/HEProgram.java:78 (GLContext.getCapabilities().OpenGL32 && !HEConfig.useLimitedRendering) || HEConfig.forceOpenGL 着色器程序
client/renderer/HEWaterRenderer.java:49 GLContext.getCapabilities().OpenGL32 && !HEConfig.useLimitedRendering 渲染器主体
client/renderer/HETessalator.java:124 !GLContext.getCapabilities().OpenGL30 || HEConfig.useLimitedRendering 取反写法(GL 3.0 判定 + 受限档)

⚠️ HEProgram 的条件与另两处不同:它多了一个 || HEConfig.forceOpenGL。 也就是说「GL 版本不足 3.2 但用户开了 forceOpenGL」时, 只有 HEProgram 会走完整路径,HEWaterRenderer 仍走受限路径—— 两者可能不匹配。源码无注释解释。

三处还分别用了 OpenGL32 与 OpenGL30 两个不同阈值, 存在「HETessalator 判定为不支持、HEWaterRenderer 判定为支持」的中间地带。

forceOpenGL 的用途与风险

配置注释(HEConfig.java:130-131):

"[CLIENT] Some Macs may always report OpenGL 2.1 - activate this to disable the OpenGL 3.2 check; it will assume you have OpenGL 3.2 or greater. But be warned: it may crash!"

即部分 Mac 只报告 OpenGL 2.1,开这个开关可跳过版本检查。源码自己警告可能崩溃。

⚠️ Angelica 强制受限渲染

if (Loader.isModLoaded("angelica")) useLimitedRendering = true;

(config/HEConfig.java:124)

装了 Angelica 就无条件走受限渲染路径,且该行在读取配置项之后执行 (HEConfig.java:122),不会写回配置文件—— 表现为「配置文件写 false,实际是 true」。

Angelica 在本 wiki 有收录(分类 angelica),跨分类链接见 分类页。 本条目不复述 Angelica 自身功能。

光照管理 HELightManager

水面高度变化会改变水下区域的透光性,因此需要重算光照。 本 mod 自己维护了一套光照标记与延迟机制。

核心静态字段(client/light/HELightManager.java:29-37)

字段 类型 说明
waterLevelOfLastUpdate float[HEConfig.maxDams] 每个水体的上次更新水位
timestampsNextUpdate long[HEConfig.maxDams] 每个水体的下次允许更新时间
chunks HashMap<Long, HELightChunk> 受影响区块缓存
maxAvailableBuffers 16 可复用缓冲区上限
availableBuffers Deque<HELightChunk> 空闲缓冲区池

waterLevelOfLastUpdate 与 timestampsNextUpdate 的长度在类加载时 由 HEConfig.maxDams 决定(HELightManager.java:29-30), 与 HE.waterBlocks 一样受 maxDams 约束。

公开方法

方法 行 作用
onChunkUnload(int, int) 39 清理该区块记录
onChunkDataLoad(Chunk) 47 区块数据载入
onSetBlock(int, int, int, Block, Block) 75 方块变化
onLightUpdate(Chunk, int, int, int) 107 原版光照更新
onPreRender(World, int, int, int) 117 渲染前
onTick() 142 每 tick 驱动
triggerLightingUpdate(int waterId, float waterLevel, float oldWaterLevel) 158 水位变化触发重算

每子区块的延迟累加(HELightManager.java:185–209)

timestampsNextUpdate[waterId] += HEConfig.minLightUpdateTimePerSubChunk;

同一行在 triggerLightingUpdate 内出现 5 次(185、194、199、204、209), 对应不同的子区块分支——即每重渲染一个 16 格高的子区块, 就追加一次 minLightUpdateTimePerSubChunk 毫秒的延迟。

配置注释(HEConfig.java:110-113)明确: "Light updates will not be lost, just delayed. ... You should expect the number of rerendered subChunks to be in the low hundreds."

📌 因此延迟不是固定值,而是 重渲染子区块数 × minLightUpdateTimePerSubChunk。 默认 10 ms × 数百个子区块 → 可达数秒。

HELightChunk 内部(HELightManager.java:242-243):

public BitSet[] lightFlags;
public short subChunkHasWaterFlags;

lightFlags 每项长度 HE.blockPerSubChunk(= 4096,HELightManager.java:259); skyLightArray 用 new NibbleArray(HE.blockPerSubChunk, 4)(377、391)。

客户端数据同步 HEClient

方法 行 作用
onWaterUpdate(int waterId, float waterLevel) 22 收到 HEPacketWaterUpdate
onConfigUpdate(waterId, blockX, blockY, blockZ, mode, limitWest, …) 30 收到 HEPacketConfigUpdate
onSynchronize(int[] blocksX, int[] blocksY, int[] blocksZ, float[] waterLevels, …) 58 登录全量同步
getAllWaterLevelForPhysicsAndLighting() 50 批量取水位
getDam(int waterId) 92 取客户端 HEDam
getWaterId(int, int, int) 103 由坐标反查 waterId
onDisconnect() 96 断线清理

水体方块 的 HEWater.getWaterLevel() 就是靠 HE.logicalClientLoaded 分流(blocks/HEWater.java:77-83):

public float getWaterLevel() {
    if (HE.logicalClientLoaded) {
        return HEClient.getDam(waterId).getWaterLevelForPhysicsAndLighting();
    } else {
        return HEServer.instance.getWaterLevel(getWaterId());
    }
}

即客户端已加载时走客户端 HEDam,否则回退到服务端实例。

客户端侧 HEDam 在 client/HEDam.java。

水下光照深度

public static final int waterOpacity = 3;
public static final int underWaterSkylightDepth = (int) Math.ceil(16f / waterOpacity);

(HE.java)

ceil(16f / 3) = 6——即穿过 16 格高的水需要跨越 6 个子区块。 对应 水体方块 的 getLightOpacity() 客户端返回 HE.waterOpacity(3)。

扩散节流

if (currentTime - timestampLastQueueTick < HEConfig.delayBetweenSpreadingChunks) {

(server/HEBlockQueue.java:35)

服务端 HEBlockQueue 用 delayBetweenSpreadingChunks(默认 2000 ms) 限制每处理一个区块的最小间隔。配置注释警告 "a single tick takes care of a whole chunk between y=0 and y=255 at once!" (HEConfig.java:101)——单 tick 就要处理整条 256 格高的列。

HE.DEBUGslowFill(HE.java:49,默认 false)在 hooks/HEHooksFML.java:30 被读取,用于放慢填充以便调试。

关联