Dynamic Lights(动态光源)
基本信息
| 属性 | 值 |
|---|---|
| 目录 | com.gtnewhorizons.angelica.dynamiclights + dynamiclights/config/ |
| 文件数 | 9(7 个根目录 + 2 个 config/) |
| 总行数 | 1299 |
| 总开关 | AngelicaConfig.enableDynamicLights(默认 true,AngelicaConfig.java:210-212) |
| 入口 | EntityLightConfig.init(new File(mc.mcDataDir, "config"))(ClientProxy.java:193) |
| 配置文件 | angelica-dynamiclights-entities.txt(EntityLightConfig.java 构造) |
它解决什么问题
原版 1.7.10 的光照是烘焙进 chunk 的:火把、灯笼之类的方块光源写死在方块状态里,手持火把 / 掉落物 / 实体发光不会照亮周围。Dynamic Lights 让任意实体或物品光源能在运行期提供光照。
9 个文件全清单(集合运算)
| # | 文件 | 行数 | 角色 |
|---|---|---|---|
| 1 | DynamicLights.java |
669 | 核心单例:光源集合、光照计算、调度 |
| 2 | ChunkRebuildManager.java |
152 | 待重建区块的排队与节流 |
| 3 | AdaptiveTickCalculator.java |
70 | 按距离/朝向决定 tick 频率 |
| 4 | AdaptiveTickMode.java |
32 | tick 频率枚举(4 项) |
| 5 | DynamicLightsMode.java |
36 | 用户可选模式枚举(5 项) |
| 6 | IDynamicLightSource.java |
91 | 光源 SPI(mod 实现的接口) |
| 7 | IDynamicLightWorldRenderer.java |
17 | 支持 chunk 重建的世界渲染器接口 |
| 8 | config/EntityLightConfig.java |
185 | 按实体类型禁用光源的配置文件读写 |
| 9 | config/EntityTypeEntry.java |
47 | 配置文件的一行记录 |
核心类:DynamicLights(669 行)
5 个静态状态字段(:47-52)
| 字段 | 初值 | 行 | 语义 |
|---|---|---|---|
Mode |
DynamicLightsMode.OFF |
:47 |
用户选择的更新模式 |
ShaderForce |
false |
:48 |
着色器强制开启 |
FrustumCullingEnabled |
true |
:49 |
视锥剔除(省重建) |
AdaptiveTickingEnabled |
true |
:50 |
自适应 tick 频率 |
configEnabled |
AngelicaConfig.enableDynamicLights |
:52 |
static final 配置快照 |
configEnabled 是 public static final(:52)——在类初始化时对 AngelicaConfig.enableDynamicLights 取一次快照,之后不可变。与其它可运行期读取的开关(FrustumCullingEnabled)性质不同。
光照半径(:54-55)
private static final double MAX_RADIUS = 7.75;
private static final double MAX_RADIUS_SQUARED = MAX_RADIUS * MAX_RADIUS;
衰减公式(:322-323):multiplier = 1.0 - sqrt(distanceSquared) / MAX_RADIUS,即半径边缘处衰减到 0。
:378 的注释给出一个具体推导:「Add MAX_RADIUS to get the search radius (13.86 + 7.75)^2 ≈ 467」——即搜索半径按 (13.86 + 7.75)² ≈ 467 选取。13.86 对应水平方向的最远对角距离。
3 个 maxDynamicLightLevel 重载
| 重载 | 行 | 用途 |
|---|---|---|
maxDynamicLightLevel(int x, int y, int z, source, current) |
:310-334 |
按整数坐标(偏 0.5,:314-316)—— 面向方块位置 |
maxDynamicLightLevelExact(double x, double y, double z, source, current) |
:336-361 |
按精确 double 坐标(不偏 0.5,:339-341) |
maxDynamicLightLevel(BlockPos, source, current) |
:363 |
便捷重载 |
两个整数/浮点版本的唯一差别是 +0.5 偏移 —— 前者把光源位置对齐到方块中心,后者保留亚方块精度。
两个枚举
DynamicLightsMode(5 项,用户可见)
| 项 | 显示名 | delay(tick) |
行 |
|---|---|---|---|
OFF |
Off |
0 | :7 |
FASTEST |
Fastest |
500 | :8 |
FAST |
Fast |
250 | :9 |
FANCY |
Fancy |
50 | :10 |
REALTIME |
Realtime |
0 | :11 |
isEnabled()(:33-35)判据是 !equals(OFF)。注意 delay 语义反直觉:FASTEST 的 delay 是 500 tick(更新最慢),REALTIME 是 0(每 tick)。即 delay 是「等待多少 tick 才更新一次」。
AdaptiveTickMode(4 项,内部)
| 项 | delay |
行 |
|---|---|---|
REAL_TIME |
1 | :8 |
SLOW |
5 | :10 |
SLOWER |
10 | :12 |
BACKGROUND |
20 | :14 |
shouldTickThisFrame(worldTick, sourceHash)(:26-31):(worldTick % delay) == Math.abs(sourceHash % delay) —— 用光源哈希错开同一模式内各光源的更新帧,避免所有光源同帧更新造成尖峰。
AdaptiveTickCalculator 的距离阈值
:11-13 的 static int(非 final,注释标注「configurable via settings」):
| 字段 | 初值 | 约合区块数 | 行 |
|---|---|---|---|
slowDistance |
32 | ~2 chunks | :11 |
slowerDistance |
64 | ~4 chunks | :12 |
backgroundDistance |
128 | ~8 chunks | :13 |
已知问题:这三个字段是 static int 而非 static final,:19-21 的三个 *Sq 平方值却只在类初始化时算一次。若运行期通过反射改写 slowDistance,*Sq 不会同步更新,calculate() 仍按旧阈值判断。注释说「configurable via settings」但仓库内无任何写这三个字段的代码。
判定顺序(calculate):先算水平点积 dot = dx*lookDirX + dz*lookDirZ,dot < 0(在相机背后)直接返回 BACKGROUND(:37-40);否则按距离降级 BACKGROUND > SLOWER > SLOW > REAL_TIME。
ChunkRebuildManager
| 成员 | 行 | 说明 |
|---|---|---|
pendingRebuilds |
:19 |
Long2IntOpenHashMap,key = 打包坐标,value = 已等待 tick 数 |
lock |
:20 |
ReentrantReadWriteLock |
maxTicksWaiting |
:23 |
static int = 100,超过则放弃该区块重建 |
candidates / toRebuild / toIncrement |
:26-28 |
3 个 LongArrayList 复用缓冲 |
requestRebuild(:34)用 CoordinatePacker.pack(x, y, z) 打包坐标。processVisible(Viewport, renderer)(:45)遍历待办,用 long2IntEntrySet().fastIterator()(:57)避免装箱。
IDynamicLightSource:mod 侧 SPI
91 行,7 个抽象方法 + 4 个 default。方法名统一用 angelica$ 前缀(Angelica 的 mixin 命名约定,避免与 mod 自身方法冲突)。
| 方法 | 行 | 性质 |
|---|---|---|
angelica$getDynamicLightX/Y/Z() |
:13/:20/:27 |
抽象 |
angelica$getDynamicLightWorld() |
:34-36 |
default → DynamicLights.getActiveRenderer() |
angelica$isDynamicLightEnabled() |
:43-45 |
default → 查全局集合 |
angelica$setDynamicLightEnabled(boolean) |
:54-61 |
@ApiStatus.Internal,Javadoc 明确警告「不要在你的 mod 里调这个,否则会搞坏东西」 |
angelica$resetDynamicLight() |
:63 |
抽象 |
angelica$getLuminance() |
:71 |
抽象,Javadoc:「最大 15,低于 1 的值被忽略」 |
angelica$dynamicLightTick() |
:76 |
抽象 |
angelica$updateLights() |
:77 |
default no-op |
angelica$updateDynamicLight(renderer) |
:79 |
抽象 |
angelica$scheduleTrackedChunksRebuild(renderer) |
:81 |
抽象 |
angelica$getLuminanceRGB() |
:87-90 |
default → BlockLightProvider.packRGB(l,l,l)(白光) |
跨子系统耦合点:angelica$getLuminanceRGB() 的 default 实现调用 API 层 的 BlockLightProvider.packRGB —— dynamiclights 直接依赖 api 的颜色打包约定。
耦合点汇总
| 方向 | 目标 | 位置 |
|---|---|---|
| 出 → 渲染 | CeleritasWorldRenderer 实现 IDynamicLightWorldRenderer |
CeleritasWorldRenderer.java:158 setActiveRenderer(this) / :163 置 null |
| 出 → 渲染 | processChunkRebuilds(viewport) |
CeleritasWorldRenderer.java:292、:325(含阴影视口) |
| 出 → chunk 构建 | WorldSlice / AngelicaChunkBuildContext 缓存 DynamicLights.get() |
WorldSlice.java:215-216、AngelicaChunkBuildContext.java:108-109 |
| 出 → 调试 | CeleritasDebugScreenHandler 读 configEnabled |
:64、:134、:138 |
| 出 → 性能 | TracyFramePlots 统计 |
:284-288 |
| 出 → API | BlockLightProvider.packRGB |
IDynamicLightSource.java:89 |
| 入 → 配置 | ClientProxy.init → EntityLightConfig.init |
ClientProxy.java:193 |
| 入 → 配置 | ClientProxy 清空光源 |
ClientProxy.java:262 removeAllLightSources() |
实体配置 GUI(client/gui/,3 个文件)
client/gui/ 共 7 个文件,本子系统占用 3 个。其余 4 个未在本文展开。
| # | 文件 | 行数 | 可见性 | 角色 |
|---|---|---|---|---|
| 1 | DynamicLightsEntityScreen.java |
242 | public class extends GuiScreen |
— |
| 2 | DynamicLightsEntityFilter.java |
96 | public record |
— |
| 3 | DynamicLightsOptionPages.java |
32 | public class |
— |
DynamicLightsOptionPages:设置页的静态构建(32 行)
整个类只有一个 public static 方法 dynamicLights()(:11),返回 OptionPage(内嵌 Sodium 的类,me.jellysquid.mods.sodium.client.gui.options.OptionPage)。
4 个 OptionGroup(:13-30):
| 组 | 选项 | 行 |
|---|---|---|
| 1 | DYNAMIC_LIGHTS、DYNAMIC_LIGHTS_SHADER_FORCE |
:14-15 |
| 2 | DYNAMIC_LIGHTS_FRUSTUM_CULLING、DYNAMIC_LIGHTS_ADAPTIVE_TICKING、DYNAMIC_LIGHTS_CULL_TIMEOUT |
:19-21 |
| 3 | DYNAMIC_LIGHTS_SLOW_DIST、DYNAMIC_LIGHTS_SLOWER_DIST、DYNAMIC_LIGHTS_BACKGROUND_DIST |
:24-26 |
| 4 | SubScreenOption → DynamicLightsEntityScreen::new |
:28 |
10 个选项全部来自 jss.notfine.core.Settings(:4 import)—— 即 NotFinite 的 Settings 枚举,不是 AngelicaConfig。⚠️ 动态光相关的 10 个设置项在 NotFinite 枚举里,不在 AngelicaConfig —— 见 NotFinite。
⚠️ ImmutableList.of(...) 硬编码 4 组(:12)—— 增删选项需改此方法。每次调用都新建 OptionPage 与 4 个 OptionGroup(无缓存)—— ⚠️ dynamicLights() 若被多次调用会重复构建(调用点不在本条目范围)。
⚠️ 本地化 key 有 3 个(:12、:28 两次):options.dynamiclights.page、options.dynamiclights.entities、options.dynamiclights.entities.tooltip —— 见 资源与语言键。
DynamicLightsEntityFilter:不可变过滤器 record(96 行)
public record DynamicLightsEntityFilter(List<String> modPrefixes, List<String> nameTerms, State state) { // :7
public enum State { ANY, ENABLED, DISABLED } // :9-13
private static final Pattern WHITESPACE = Pattern.compile("\\s+"); // :15
private static final DynamicLightsEntityFilter MATCH_ALL = new DynamicLightsEntityFilter(List.of(), List.of(), State.ANY); // :16
3 个组件 + 嵌套枚举 State(3 值)。
紧凑构造器做防御性拷贝(:18-21):
public DynamicLightsEntityFilter {
modPrefixes = List.copyOf(modPrefixes);
nameTerms = List.copyOf(nameTerms);
}
⚠️ List.copyOf 会拒绝 null 元素(抛 NPE)—— 比 Collections.unmodifiableList 更严格。这是有意的强不可变。
parse(String query) 的三个早退(:23-):
| 输入 | 返回 | 行 |
|---|---|---|
null |
MATCH_ALL |
:24-26 |
strip().toLowerCase(Locale.ROOT) 后为空 |
MATCH_ALL |
:29-31 |
| 正常 | 解析出的 filter | :33- |
⚠️ toLowerCase(Locale.ROOT)(:28)—— 显式指定 ROOT 避免土耳其语 I 问题,是正确的。
⚠️ ⚠️ String.strip()(:28)是 Java 11+,String.isBlank() 亦然。本项目的编译 JDK 版本未独立核实(version_gate.py 只确认 minecraftVersion = 1.7.10)。⚠️ WHITESPACE 的 Pattern(:15)在已读的 :15-33 范围内只被声明,未见使用 —— 它应在 :33 之后的 parse 分词逻辑里使用,该部分本条目未读。⚠️ 同时用 strip() 和 WHITESPACE 正则做空白处理—— 可能冗余,也可能用于 strip() 之外的内部拆分。
⚠️ ⚠️ MATCH_ALL 是 static final 单例(:16)—— 但它用的是 List.of()(不可变空列表)且通过了紧凑构造器的 List.copyOf,安全可共享。⚠️ 相比之下 parse(null) 直接返回 MATCH_ALL 而非新建(:25)—— 正确(不可变值可安全共享)。
⚠️ parse 的返回类型是 DynamicLightsEntityFilter(record),语义为「包含规则」还是「排除规则」由 State 三值区分(ANY / ENABLED / DISABLED)。⚠️ 匹配逻辑(modPrefixes 前缀匹配 / nameTerms 词匹配 / State 过滤)的方法在 :33 之后,本条目未读。
DynamicLightsEntityScreen:实体列表 GUI(242 行)
public class DynamicLightsEntityScreen extends GuiScreen { // :22
private static final String ENABLED_PREFIX = EnumChatFormatting.GREEN + "[x] " + EnumChatFormatting.WHITE; // :24
private static final String DISABLED_PREFIX = EnumChatFormatting.DARK_GRAY + "[ ] " + EnumChatFormatting.GRAY; // :25
两个复选框前缀字符串([x] / [ ])—— ENABLED_PREFIX 是绿色,DISABLED_PREFIX 是深灰,与 State 枚举的 3 值对应 Enabled/Disabled 两态显示(ANY 态在 GUI 上如何呈现本条目未读)。
⚠️ ⚠️ 构造器 DynamicLightsEntityScreen(GuiScreen parent)(:42) —— 接受父界面,说明它是从设置页 SubScreenOption push 出来的(对应 DynamicLightsOptionPages.java:28)。⚠️ 与 Iris 的 IrisGuiSlot 有关联:内部类 EntityRowList extends IrisGuiSlot(:195)—— ⚠️ ⚠️ 这是「内嵌 Iris 的 GUI 基类」,但本条目未读其 import,无法确认包名。IrisGuiSlot 的定义不在本条目覆盖范围。
已确认的方法(18 个):
| 方法 | 行 | 类别 |
|---|---|---|
DynamicLightsEntityScreen(GuiScreen) |
:42 |
构造 |
initGui() |
:55 |
覆写 |
onGuiClosed() |
:71 |
覆写 |
onClose() |
:75 |
私有 |
refilter() |
:83 |
私有 |
updateToggleButton() |
:109 |
私有 |
toggleVisible() |
:123 |
私有 |
drawScreen(int, int, float) |
:133 |
覆写 |
updateScreen() |
:148 |
覆写 |
mouseClicked(int, int, int) |
:153 |
覆写 |
mouseMovedOrUp(int, int, int) |
:161 |
覆写 |
actionPerformed(GuiButton) |
:168 |
覆写 |
keyTyped(char, int) |
:175 |
覆写 |
EntityRowList extends IrisGuiSlot |
:195 |
内部类 |
drawBackground() |
:213 |
覆写,空实现 |
⚠️ drawBackground() 是空实现(:213,{})—— 刻意不画默认背景(自定义布局)。这是覆写而非遗漏。
⚠️ keyTyped(char, int)(:175)同时处理搜索框输入与翻页 —— ⚠️ 1.7.10 的 keyTyped 签名是 (char, int),现代版是 (char, int, ScanEvent)。这是 1.7.10 特有签名,正确。
⚠️ EntityRowList 是 private 非静态内部类(:195)—— 隐式持有外层 DynamicLightsEntityScreen 实例(drawScreen 需访问 refilter / toggleVisible)。⚠️ 本条目未读 :195-242 的 47 行(EntityRowList 的实体行渲染逻辑)—— 本条目的已知缺口。
⚠️ 全部 UI 文本硬编码英文或走 I18n.format(DynamicLightsOptionPages 走 I18n,DynamicLightsEntityScreen 的 [x]/[ ] 是硬编码)—— 两种风格并存。
已知问题
configEnabled是static final快照(:52):类初始化后不可变。仓库内AngelicaConfig由 coremod 阶段装载(AngelicaClientTweaker:71),早于本类初始化,因此实际安全——但这是隐式时序依赖,无断言保障。AdaptiveTickCalculator的*Sq字段不随*字段更新(见上),是真实的静态状态一致性缺陷。angelica$getLuminance()下限未强制:Javadoc 说「低于 1 的值被忽略」(:68),但接口无@Min约束,mod 实现返回 0 不会被拒绝。EntityLightConfig用EntityRegistry.instance().lookupModSpawn(...)(:108)读取实体所属 mod —— 这是查询不是注册,不要误计为 Entity 注册(见 §④ 裁定)。ChunkRebuildManager.maxTicksWaiting是static int= 100(:23),有setMaxTicksWaiting(:30)可改,但无上限校验。AdaptiveTickMode只有 4 档、DynamicLightsMode只有 5 档,两套枚举的delay语义不同(后者是毫秒级 50/250/500,前者是 tick 级 1/5/10/20),容易混淆。DynamicLightsEntityFilter同时用String.strip()(Java 11+)与WHITESPACE正则(:15、:28)做空白处理,可能冗余;本项目的编译 JDK 版本未独立核实。DynamicLightsEntityScreen.EntityRowList extends IrisGuiSlot(:195)—— 「内嵌 Iris 的 GUI 基类」被 GUI 列表继承,但IrisGuiSlot的包名与定义不在本条目覆盖范围(未读)。:195-242的 47 行本条目未读。DynamicLightsOptionPages.dynamicLights()每次调用都重建OptionPage+ 4 个OptionGroup(:11-31),无缓存。- 10 个动态光设置项在 NotFinite 的
Settings枚举里,不在AngelicaConfig(DynamicLightsOptionPages.java:4、:14-26)。 DynamicLightsEntityScreen的[x]/[ ]前缀硬编码(:24-25),而DynamicLightsOptionPages走I18n.format—— 两种本地化风格并存。
相关条目
- API 层 -
BlockLightProvider.packRGB、IDynamicLightProducer - CompatConfig - 第三方 mod 兼容开关
- AngelicaModulesConfig -
enableDynamicLights - Proxy -
EntityLightConfig.init与清理调用点 - KeyBinding 注册总表 - 本子系统不注册按键
- NotFinite - 10 个动态光设置项的真正定义处
- Iris(内嵌) -
IrisGuiSlot(GUI 列表基类)的来源 - 资源与语言键 -
options.dynamiclights.*的 3 个 key - 加载控制器与加载阶段 - GUI 相关的类初始化时机