客户端渲染与光照
基本信息
| 属性 | 值 |
|---|---|
| 客户端入口 | 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 被读取,用于放慢填充以便调试。