Files
xczjhk/_gen_amb.py
2026-10-09 18:23:57 +08:00

497 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""救护车一次性生成器 (v3,支持多车型):
对 CARS 里的每个车型:
1) 依据其源模型(用户原始模型,已按网易规则做过 X 镜像)逐立方体做层级变换,
得到世界空间 AABB。关键:网易方块实体的渲染映射为 world = (-geo_x/16, geo_y/16, geo_z/16),
即 X 轴镜像;因此碰撞检测必须使用镜像后的世界坐标。
2) 对 4 个车体旋转角 φ(0/90/180/270) 分别求「每格与车体的交集 AABB」,量化到 1/QUANT 格并去重;
几何体根骨骼 Y 角设为 90 - φ(才能得到世界旋转 φ),并把整车居中到主方块中心、车底贴地。
3) 生成:每个车型一份预旋转几何体(4 条,按朝向)、4 个主方块 + 若干形状共享占位方块;
汇总生成 blocks.json、terrain_texture.json,并把布局数据写入 ambulanceServer.py 末尾。
车型命名规则(tag):
主方块 dn_sa:<tag>_main_<yaw>
占位方块 dn_sa:<tag>_ph_<NN> (形状共享,仅提供碰撞)
几何体 geometry.dn_sa_<tag>_<yaw> -> resource_pack/models/entity/<tag>_geo.json
client_entity entity/<tag>_main_<yaw>_block_entity.json
"""
import json
import math
import os
import io
ROOT = r"e:\MCStudioDownload\work\3027246179@qq.com\Cpp\AddOn\0aaaa4f2f16c46e8964025a5ab0ecc02"
BP = os.path.join(ROOT, "behavior_pack_kcH4M5u0")
RP = os.path.join(ROOT, "resource_pack_t7eJjrxY")
BEH_BLOCKS_DIR = os.path.join(BP, "netease_blocks")
ENTITY_DIR = os.path.join(RP, "entity")
MODELS_DIR = os.path.join(RP, r"models\entity")
# ---------------------------------------------------------------- 车型配置
# tag 决定所有资源的命名;src 是源模型;tex 是该车的整车贴图(client_entity 用)
CARS = [
{
"item": "dn_sa:ambulance_car",
"tag": "ambulance_car",
"src": os.path.join(ROOT, "_gen_amb_src.geo.json"),
"tex": "textures/blocks/ambulance_car",
},
{
"item": "dn_sa:ambulance_car_broken",
"tag": "ambulance_car_broken",
"src": os.path.join(ROOT, "_gen_amb_src_broken.geo.json"),
"tex": "textures/blocks/ambulance_broken",
},
{
"item": "dn_sa:ambulance_car_destroyed",
"tag": "ambulance_car_destroyed",
"src": os.path.join(ROOT, "_gen_amb_src_destroyed.geo.json"),
"tex": "textures/blocks/ambulance_destroyed",
},
]
# 模型原点(0,0,0) 对应的主方块格上的位置(最小角)。若游戏内整车整体偏移,改这里即可。
ANCHOR = (0.0, 0.0, 0.0)
QUANT = 8 # 碰撞盒量化精度:1/8 格(约 2 个贴图像素)
YAWS = (0, 90, 180, 270) # 车体在世界空间的旋转角 φ(几何体根骨骼 Y 角 = 90 - φ)
# 玩家朝向象限(0=南+Z,90=西-X,180=北-Z,270=东+X) -> 车体世界旋转角 φ
# 依据:φ=0 时车头(capo)朝 -X;把 -X 经 Ry(φ) 转到玩家朝向
EXTRA_BY_FACING = {0: 90, 90: 0, 180: 270, 270: 180}
# 「车体局部格」(φ=0 坐标系,相对主方块的偏移) —— 这些格子不生成占位方块。
# 1) 挡风玻璃前上方 / 引擎盖正上方那一片:玻璃是斜的(rot -60),它的整体包围盒把它前上方
# 那几格也算了进去,但那里几乎全是空气,碰撞盒会悬在车外。
# 2) 引擎盖最前面(x=-3) 顶层(y=2):该层只有车头那 0.3 格高的一小条,比它后面一格
# (x=-2 顶层已被剔除) 高出来一截,看起来像引擎盖前端多出一块,一并剔除。
# 3) 警灯上方(x=-1) 顶层(y=3):驾驶室顶只有 2.79 高、警灯顶 3.02,这一层(3.0~3.5)全部
# 来自斜挡风玻璃包围盒的溢出,正好悬在警灯上方,看起来像警灯位置高出来一块。
SKIP_LOCAL = {
(-2, 2, -1), (-2, 2, 0), (-2, 2, 1),
(-2, 3, -1), (-2, 3, 0), (-2, 3, 1),
(-3, 2, -1), (-3, 2, 0), (-3, 2, 1),
(-1, 3, -1), (-1, 3, 0), (-1, 3, 1),
}
BLANK_MODEL = "dn_sa:ambulance_blank_model"
# 主方块碰撞盒模式(网易方块实体的模型可见性跟着「方块所在位置」走,主方块碰撞盒太小时,
# 玩家把屏幕转开、方块位置离开视野,整车的实体模型就会停止渲染而消失):
# "model":给主方块一个涵盖整车的碰撞盒(解决上述消失问题)
# "cell" :只用主方块自己那一格的碰撞盒(旧行为,其余碰撞靠占位方块)
MAIN_BOX_MODE = "model"
def model_box(bones, anchor):
"""整车在世界空间(相对主方块)的包围盒 (lo, hi)。"""
boxes = world_boxes(bones, False) # 含零厚度面片,才能真的涵盖整个模型
lo = [min(bx[0][i] for bx in boxes) + anchor[i] for i in range(3)]
hi = [max(bx[1][i] for bx in boxes) + anchor[i] for i in range(3)]
return lo, hi
def skip_cells(phi):
"""把车体局部格按 yaw 旋转成世界格(与几何体同向)。"""
out = set()
for (x, y, z) in SKIP_LOCAL:
if phi == 0:
ax, az = x, z
elif phi == 90:
ax, az = z, -x
elif phi == 180:
ax, az = -x, -z
else:
ax, az = -z, x
out.add((ax, y, az))
return out
# ---------------------------------------------------------------- math
def mat_mul(a, b):
return [[sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)] for i in range(3)]
def rot_axis(axis, deg):
t = math.radians(deg)
c, s = math.cos(t), math.sin(t)
if axis == 'x':
return [[1, 0, 0], [0, c, -s], [0, s, c]]
if axis == 'y':
return [[c, 0, s], [0, 1, 0], [-s, 0, c]]
return [[c, -s, 0], [s, c, 0], [0, 0, 1]]
def euler(rx, ry, rz):
m = rot_axis('y', ry)
m = mat_mul(m, rot_axis('x', rx))
m = mat_mul(m, rot_axis('z', rz))
return m
def apply(m, v, pivot):
d = [v[i] - pivot[i] for i in range(3)]
return [sum(m[i][k] * d[k] for k in range(3)) + pivot[i] for i in range(3)]
def cube_corners(cube):
o = cube['origin']
s = cube['size']
pt = cube.get('pivot', [0, 0, 0])
pts = [[o[0] + s[0] * dx, o[1] + s[1] * dy, o[2] + s[2] * dz]
for dx in (0, 1) for dy in (0, 1) for dz in (0, 1)]
rot = cube.get('rotation')
if rot and any(abs(v) > 1e-9 for v in rot):
m = euler(rot[0], rot[1], rot[2])
pts = [apply(m, p, pt) for p in pts]
return pts
def bone_world(by_name, bone, p):
b = bone
while b is not None:
rot = b.get('rotation')
if rot and any(abs(v) > 1e-9 for v in rot):
m = euler(rot[0], rot[1], rot[2])
p = apply(m, p, b.get('pivot', [0, 0, 0]))
b = by_name.get(b.get('parent'))
return p
def world_boxes(bones, skip_degenerate=True):
by_name = {b['name']: b for b in bones}
out = []
for b in bones:
for cube in b.get('cubes', []):
pts = [bone_world(by_name, b, p) for p in cube_corners(cube)]
# 网易方块实体:世界 X = -几何体 X(镜像),Y/Z 不变
pts = [[-p[0] / 16.0, p[1] / 16.0, p[2] / 16.0] for p in pts]
lo = [min(p[i] for p in pts) for i in range(3)]
hi = [max(p[i] for p in pts) for i in range(3)]
if skip_degenerate and (hi[0] - lo[0] < 1e-9 or hi[1] - lo[1] < 1e-9
or hi[2] - lo[2] < 1e-9):
continue # 零厚度面片不产生碰撞
out.append((lo, hi))
return out
def cells_for(boxes, anchor):
cells = {}
for lo, hi in boxes:
lo = [lo[i] + anchor[i] for i in range(3)]
hi = [hi[i] + anchor[i] for i in range(3)]
for ix in range(int(math.floor(lo[0])), int(math.floor(hi[0] - 1e-9)) + 1):
for iy in range(int(math.floor(lo[1])), int(math.floor(hi[1] - 1e-9)) + 1):
for iz in range(int(math.floor(lo[2])), int(math.floor(hi[2] - 1e-9)) + 1):
key = (ix, iy, iz)
l = [max(lo[0], ix), max(lo[1], iy), max(lo[2], iz)]
h = [min(hi[0], ix + 1), min(hi[1], iy + 1), min(hi[2], iz + 1)]
if key not in cells:
cells[key] = [l, h]
else:
pl, ph = cells[key]
cells[key] = [[min(pl[i], l[i]) for i in range(3)],
[max(ph[i], h[i]) for i in range(3)]]
return cells
def q(v, quant):
return round(v * quant) / float(quant)
def local_aabb(cell, l, h, quant=QUANT):
"""返回该格内 0~1 的局部 AABB(量化 1/quant),退化则返回 None。"""
lo = [min(1.0, max(0.0, q(l[i] - cell[i], quant))) for i in range(3)]
hi = [min(1.0, max(0.0, q(h[i] - cell[i], quant))) for i in range(3)]
if any(hi[i] - lo[i] <= 1e-6 for i in range(3)):
return None
return tuple(lo + hi)
# ---------------------------------------------------------------- per-car 计算
def load_model(path):
with io.open(path, 'r', encoding='utf-8') as f:
raw = json.load(f)
geo = raw['minecraft:geometry'][0]
root = [b['name'] for b in geo['bones'] if 'parent' not in b][0]
return geo, root
def bones_for_yaw(base_geo, root_name, phi):
"""车体世界旋转 φ 时对应的几何体骨骼:根骨骼 Y=90-φ,并把整车居中到主方块中心、车底贴地。"""
bones = json.loads(json.dumps(base_geo['bones']))
for b in bones:
if b['name'] == root_name:
r = list(b.get('rotation', [0, 0, 0]))
b['rotation'] = [r[0], r[1] - phi, r[2]]
# 求世界包围盒(已含 X 镜像,含零厚度面片),据此算居中/贴地平移量
boxes = world_boxes(bones, False)
lo = [min(bx[0][i] for bx in boxes) for i in range(3)]
hi = [max(bx[1][i] for bx in boxes) for i in range(3)]
cx = (lo[0] + hi[0]) / 2.0
cz = (lo[2] + hi[2]) / 2.0
tw = (0.5 - cx, -lo[1], 0.5 - cz) # 期望世界平移:X/Z 居中,车底 y=0
shift = [-16.0 * tw[0], 16.0 * tw[1], 16.0 * tw[2]] # 几何体空间平移 Δ = 16*diag(-1,1,1)*tw
for b in bones:
p = b.get('pivot', [0, 0, 0])
b['pivot'] = [p[i] + shift[i] for i in range(3)]
for c in b.get('cubes', []):
o = c['origin']
c['origin'] = [o[i] + shift[i] for i in range(3)]
if 'pivot' in c: # 立方体自带旋转轴心也需平移,否则非纯平移
cp = c['pivot']
c['pivot'] = [cp[i] + shift[i] for i in range(3)]
return bones
def wjson(path, obj):
with io.open(path, 'w', encoding='utf-8') as f:
f.write(json.dumps(obj, indent=2, ensure_ascii=False))
f.write(u"\n")
def block_json(identifier, aabb):
lo, hi = aabb
return {
"format_version": "1.10.0",
"minecraft:block": {
"description": {
"identifier": identifier,
"register_to_creative_menu": False,
"is_experimental": False,
"category": "dn_sa",
},
"components": {
"netease:solid": {"value": True},
"netease:render_layer": {"value": "alpha"},
"minecraft:destroy_time": {"value": 1.0},
"minecraft:block_light_absorption": {"value": 0},
"netease:aabb": {
"collision": {"min": list(lo), "max": list(hi)},
"clip": {"min": list(lo), "max": list(hi)},
},
},
},
}
# 清理可能残留的旧文件(旧命名 / 旧的占位方块 / 旧的渲染锚点方块与客户端实体)
for f in os.listdir(BEH_BLOCKS_DIR):
if not f.endswith(".json"):
continue
if f == "ambulance_car_main.json" or ("ambulance_car" in f and "_ph_" in f):
os.remove(os.path.join(BEH_BLOCKS_DIR, f))
for f in os.listdir(ENTITY_DIR):
if not f.endswith(".json"):
continue
if "ambulance_car" in f and f.endswith("_block_entity.json"):
os.remove(os.path.join(ENTITY_DIR, f))
old_ent = os.path.join(ENTITY_DIR, "ambulance_car_main_block_entity.json")
if os.path.exists(old_ent):
os.remove(old_ent)
results = []
for car in CARS:
tag = car["tag"]
main_prefix = "dn_sa:%s_main" % tag
ph_prefix = "dn_sa:%s_ph_" % tag
geo_id = "geometry.dn_sa_%s" % tag
base_geo, root_name = load_model(car["src"])
per_yaw = {} # yaw -> {cell: local_aabb}(逐格碰撞盒)
yaw_bones = {} # yaw -> 已旋转/居中/贴地的骨骼
main_box = {} # yaw -> 整车包围盒(主方块「涵盖整个模型」碰撞盒)
shapes = {} # local_aabb -> None(形状共享)
for yaw in YAWS:
bones = bones_for_yaw(base_geo, root_name, yaw)
yaw_bones[yaw] = bones
cells = cells_for(world_boxes(bones), ANCHOR)
skip = skip_cells(yaw)
table = {}
removed = []
for cell, (l, h) in sorted(cells.items()):
if cell in skip:
removed.append(cell)
continue
la = local_aabb(cell, l, h)
if la is None:
continue
table[cell] = la
per_yaw[yaw] = table
main_box[yaw] = model_box(bones, ANCHOR)
for la in table.values():
shapes.setdefault(la, None)
print("[%s] yaw=%3d cells=%3d shapes=%3d removed=%s"
% (tag, yaw, len(table), len(set(table.values())), removed))
# 形状共享占位方块命名(每种「格内碰撞盒」一个变体,被多格复用)
names = {}
for i, shape in enumerate(sorted(shapes.keys())):
names[shape] = "%s%02d" % (ph_prefix, i)
# 每格最终方块名:主方块所在格用主方块,其余用形状共享占位方块
cellname = {}
for yaw in YAWS:
m = {}
for cell, la in per_yaw[yaw].items():
if cell == (0, 0, 0):
continue
m[cell] = names[la]
cellname[yaw] = m
print("[%s] placeholder shapes=%d" % (tag, len(names)))
# 1) 预旋转几何体:每个朝向一条,渲染整车
geos = []
for yaw in YAWS:
desc = dict(base_geo["description"])
desc["identifier"] = "%s_%d" % (geo_id, yaw)
geos.append({"description": desc, "bones": yaw_bones[yaw]})
wjson(os.path.join(MODELS_DIR, "%s_geo.json" % tag),
{"format_version": "1.21.0", "minecraft:geometry": geos})
# 2) client_entity:每个朝向一份,渲染整车
for yaw in YAWS:
name = "%s_%d" % (main_prefix, yaw)
ent = {
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": name,
"geometry": {"default": "%s_%d" % (geo_id, yaw)},
"textures": {"default": car["tex"]},
"materials": {"default": "entity_alphatest"},
"render_controllers": [
{"controller.render.dn_sa_ambulance_car_main": "variable.variant == 0"}
],
"scripts": {"initialize": ["variable.variant = 0;"]},
}
},
}
wjson(os.path.join(ENTITY_DIR, "%s_main_%d_block_entity.json" % (tag, yaw)), ent)
# 3) 方块定义:主方块(带 block_entity) + 形状共享占位方块
for yaw in YAWS:
name = "%s_%d" % (main_prefix, yaw)
if MAIN_BOX_MODE == "model":
lo, hi = main_box[yaw] # 一个碰撞盒涵盖整车
else:
la = per_yaw[yaw].get((0, 0, 0)) or (0.0, 0.0, 0.0, 1.0, 1.0, 1.0)
lo, hi = la[:3], la[3:]
obj = block_json(name, (lo, hi))
obj["minecraft:block"]["components"]["netease:block_entity"] = {"tick": False, "movable": False}
wjson(os.path.join(BEH_BLOCKS_DIR, "%s_main_%d.json" % (tag, yaw)), obj)
for shape, name in sorted(names.items(), key=lambda kv: kv[1]):
short = name.split(":", 1)[1]
wjson(os.path.join(BEH_BLOCKS_DIR, short + ".json"),
block_json(name, (shape[:3], shape[3:])))
results.append({
"item": car["item"], "tag": tag, "tex": car["tex"],
"main_prefix": main_prefix, "ph_prefix": ph_prefix,
"cellname": cellname, "names": names,
})
print("[%s] wrote geo + %d client_entity + %d block defs"
% (tag, len(YAWS), len(YAWS) + len(names)))
# ---------------------------------------------------------------- blocks.json
blocks = {"format_version": [1, 1, 0]}
blocks["dn_sa:generator"] = {
"sound": "stone",
"client_entity": {
"identifier": "dn_sa:generator",
"hand_model_use_client_entity": True,
"destroyed_textures": "dn_sa:generator_icon",
"block_icon": "dn_sa:generator_icon",
},
}
blocks["dn_sa:salvage_bin"] = {
"sound": "stone",
"client_entity": {
"identifier": "dn_sa:salvage_bin",
"hand_model_use_client_entity": True,
"destroyed_textures": "dn_sa:salvage_bin_icon",
"block_icon": "dn_sa:salvage_bin_icon",
},
}
for car in results:
for yaw in YAWS:
name = "%s_%d" % (car["main_prefix"], yaw)
blocks[name] = {
"sound": "stone",
"client_entity": {
"identifier": name,
"hand_model_use_client_entity": True,
"destroyed_textures": car["item"],
"block_icon": car["item"],
},
}
for car in results:
for shape, name in sorted(car["names"].items(), key=lambda kv: kv[1]):
blocks[name] = {"sound": "stone", "netease_model": BLANK_MODEL}
wjson(os.path.join(RP, "blocks.json"), blocks)
print("wrote blocks.json (%d entries)" % len(blocks))
# ---------------------------------------------------------------- terrain_texture.json
tt_path = os.path.join(RP, r"textures\terrain_texture.json")
with io.open(tt_path, 'r', encoding='utf-8') as f:
tt = json.load(f)
td = tt["texture_data"]
for legacy in ("dn_sa_ambulance_car_main", "dn_sa_ambulance_car_placeholder"):
td.pop(legacy, None)
for key in [k for k in td if k.startswith("dn_sa:ambulance") and "_ph_a" in k]:
td.pop(key, None) # 清掉上一次生成的渲染锚点贴图条目
for car in results:
td["dn_sa_%s" % car["tag"]] = {"textures": car["tex"]} # 方块破坏/图标贴图
for yaw in YAWS:
td["%s_%d" % (car["main_prefix"], yaw)] = {"textures": "textures/blocks/dn_sa_blank"}
wjson(tt_path, tt)
print("wrote terrain_texture.json")
# ---------------------------------------------------------------- 注入碰撞数据到 ambulanceServer.py
MARKER = u"# ==== AUTO-GENERATED LAYOUT DATA (by _gen_amb.py, do not edit) ===="
lines = []
lines.append(u"")
lines.append(MARKER)
lines.append(u"# 玩家朝向象限(0=南+Z,90=西-X,180=北-Z,270=东+X) -> 车体额外 Y 旋转角")
lines.append(u"EXTRA_BY_FACING = {")
for k in sorted(EXTRA_BY_FACING):
lines.append(u" %d: %d," % (k, EXTRA_BY_FACING[k]))
lines.append(u"}")
lines.append(u"# 物品名 -> 车型数据")
lines.append(u"CARS = {}")
for car in results:
lines.append(u"CARS['%s'] = {" % car["item"])
lines.append(u" 'main_prefix': '%s'," % car["main_prefix"])
lines.append(u" 'ph_prefix': '%s'," % car["ph_prefix"])
lines.append(u" 'main': {")
for yaw in YAWS:
lines.append(u" %d: '%s_%d'," % (yaw, car["main_prefix"], yaw))
lines.append(u" },")
lines.append(u" 'cells': {")
for yaw in YAWS:
lines.append(u" %d: {" % yaw)
for cell in sorted(car["cellname"][yaw].keys()):
if cell == (0, 0, 0):
continue
lines.append(u" (%d, %d, %d): '%s',"
% (cell[0], cell[1], cell[2], car["cellname"][yaw][cell]))
lines.append(u" },")
lines.append(u" },")
lines.append(u"}")
data_block = u"\n".join(lines)
server_path = os.path.join(BP, r"Script_dn_sa\ambulanceServer.py")
with io.open(server_path, 'r', encoding='utf-8') as f:
content = f.read()
idx = content.find(MARKER)
if idx < 0:
raise RuntimeError("marker not found in ambulanceServer.py")
with io.open(server_path, 'w', encoding='utf-8') as f:
f.write(content[:idx] + data_block + u"\n")
print("injected layout data into ambulanceServer.py")
old_layout = os.path.join(BP, r"Script_dn_sa\ambulanceLayout.py")
if os.path.exists(old_layout):
os.remove(old_layout)
print("DONE")