04 多方块模型的实现
我们说了怎么多方块机器的是怎么创建出来,以及多方块机器是怎么破坏的。不过我们没说为什么机器长这个样子,这次我们来看看沉浸的模型是怎么做的。对于模型,沉浸的机器的模型是obj文件,不过在之前的内容中我们知道机器是一个一个机器方块组成,如果直接将obj来作为方块模型那么会出现多个模型叠在一起才对。为什么在游戏中每一个方块的模型是整个obj模型的一部分?
其实沉浸自己写了一个模型的加载器,用于加载对应的json文件,在加载中不仅加载机器的obj模型,还要加载对应的位置信息,然后照着这个位置信息对整个obj模型的顶点进行切割。最后得到每一个相对的位置都是那些顶点数据,然后在渲染时候按照对应的相对位置返回对应的顶点信息进行渲染,这样显示的就是多个方块组成的一个模型。
先来看下blockstate把,这里还是用焦炉作为例子,其他的类似。
{
"variants": {
"active=false,facing=east": {
"model": "immersiveengineering:block/coke_oven_off_split",
"uvlock": true,
"y": 90
},
"active=false,facing=north": {
"model": "immersiveengineering:block/coke_oven_off_split",
"uvlock": true
},
"active=false,facing=south": {
"model": "immersiveengineering:block/coke_oven_off_split",
"uvlock": true,
"y": 180
},
"active=false,facing=west": {
"model": "immersiveengineering:block/coke_oven_off_split",
"uvlock": true,
"y": 270
},
"active=true,facing=east": {
"model": "immersiveengineering:block/coke_oven_on_split",
"uvlock": true,
"y": 90
},
"active=true,facing=north": {
"model": "immersiveengineering:block/coke_oven_on_split",
"uvlock": true
},
"active=true,facing=south": {
"model": "immersiveengineering:block/coke_oven_on_split",
"uvlock": true,
"y": 180
},
"active=true,facing=west": {
"model": "immersiveengineering:block/coke_oven_on_split",
"uvlock": true,
"y": 270
}
}
}
这个和原版没太大的区别,主要还是分为燃烧和非燃烧的状态,以及朝向不同时候模型应该怎么旋转。
这里以焦炉的燃烧状态模型作为例子:
{
"parent": "minecraft:block/block",
"dynamic": false, // 这个表示模型是否动态,指那些可以添加新的组件的部分,例如加鼓风机的高卢。
"inner_model": { // 其中这个inner_model是neoforge提供的obj加载。
"parent": "minecraft:block/block",
"automatic_culling": false,
"flip_v": true,
"loader": "neoforge:obj",
"model": "immersiveengineering:models/block/stone_multiblocks/cube_three.obj",
"textures": {
"front": "immersiveengineering:block/multiblocks/coke_oven_on",
"particle": "immersiveengineering:block/multiblocks/coke_oven_on",
"side": "immersiveengineering:block/multiblocks/coke_oven"
}
},
"loader": "immersiveengineering:basic_split", // 指定加载模型的loader
"split_parts": [
[
1,
1,
1
]....
],
"textures": { // 贴图
"particle": "immersiveengineering:block/multiblocks/coke_oven_on"
}
}
在上面的内容中,是自己自定义的加载。其中inner_model这个字段是neoforge提供的obj模型的加载。其他的是作者添加的。其中主要关注的是split_parts这一部分,这一部分说明了这个模型要按照上面样子的位置去切割对应的顶点,例如焦炉就是3 * 3 * 3的建筑,那么就是-1,-1,-1 到1,1,1的切割。
//SplitModelLoader
public static final ResourceLocation LOCATION = new ResourceLocation(ImmersiveEngineering.MODID, "basic_split");
public static final String PARTS = "split_parts";
public static final String INNER_MODEL = "inner_model";
public static final String DYNAMIC = "dynamic";
@Nonnull
@Override
public UnbakedSplitModel read(JsonObject modelContents, @Nonnull JsonDeserializationContext deserializationContext)
{
UnbakedModel baseModel;
JsonElement innerJson = modelContents.get(INNER_MODEL);
baseModel = ExtendedBlockModelDeserializer.INSTANCE.fromJson(innerJson, BlockModel.class);
JsonArray partsJson = modelContents.getAsJsonArray(PARTS);
List<Vec3i> parts = new ArrayList<>(partsJson.size());
for(JsonElement e : partsJson)
parts.add(fromJson(e.getAsJsonArray()));
BoundingBox box = pointBB(parts.get(0));
for(Vec3i v : parts)
box.encapsulate(pointBB(v));
Vec3i size = new Vec3i(box.getXSpan(), box.getYSpan(), box.getZSpan());
return new UnbakedSplitModel(baseModel, parts, modelContents.get(DYNAMIC).getAsBoolean(), size);
}
通过原版的方法解析inner_model字段获得对应的UnbakedModel实例,并读取对应的parts字段的数组,转化为vec3i的数据。通过这个parts算出最大的包围盒size。构建对应的UnbakedSplitModel。
然后就是对应的烘焙,就是构造对应可以渲染的顶点数据。
//UnbakedSplitModel
@Override
public BakedModel bake(
IGeometryBakingContext owner,
ModelBaker bakery,
Function<Material, TextureAtlasSprite> spriteGetter,
ModelState modelTransform,
ItemOverrides overrides,
ResourceLocation modelLocation
)
{
BakedModel bakedBase = baseModel.bake(bakery, spriteGetter, BlockModelRotation.X0_Y0, modelLocation);
if(dynamic)
return new BakedDynamicSplitModel<>(
(ICacheKeyProvider<?>)bakedBase, parts, modelTransform, size
);
else
return new BakedBasicSplitModel(bakedBase, parts, modelTransform, size, owner.getTransforms());
}
这里会根据是否是动态的分为两个烘焙的模型。
public class BakedBasicSplitModel extends AbstractSplitModel<BakedModel>
{
private static final Set<BakedBasicSplitModel> WEAK_INSTANCES = Collections.newSetFromMap(new WeakHashMap<>());
static
{
// 清楚缓存时候重置
IEApi.renderCacheClearers.add(() -> WEAK_INSTANCES.forEach(b -> b.splitModels.reset()));
}
private final ResettableLazy<Map<Vec3i, List<BakedQuad>>> splitModels;
private final ItemTransforms itemTransforms;
public BakedBasicSplitModel(
BakedModel base, Set<Vec3i> parts, ModelState transform, Vec3i size, ItemTransforms itemTransforms
)
{
super(base, size);
this.itemTransforms = itemTransforms;
this.splitModels = new ResettableLazy<>(() -> {
List<BakedQuad> quads = base.getQuads(null, null, ApiUtils.RANDOM_SOURCE, ModelData.EMPTY, null);
return split(quads, parts, transform);
});
WEAK_INSTANCES.add(this);
}
@Nonnull
@Override
public List<BakedQuad> getQuads(
@Nullable BlockState state, @Nullable Direction side, @Nonnull RandomSource rand,
@Nonnull ModelData extraData, @Nullable RenderType layer
)
{
BlockPos offset = extraData.get(Model.SUBMODEL_OFFSET);//检查extraData中是否存在子模型偏移。
if(offset!=null)
return splitModels.get().getOrDefault(offset, ImmutableList.of());//如果存在偏移,则从缓存的splitModels中返回相应的四边形。
else
return base.getQuads(state, side, rand, extraData, layer);//如果不存在偏移,则从基础模型返回四边形。
}
}
public BakedBasicSplitModel(
BakedModel base, Set<Vec3i> parts, ModelState transform, Vec3i size, ItemTransforms itemTransforms
)
{
super(base, size);
this.itemTransforms = itemTransforms;
this.splitModels = new ResettableLazy<>(() -> {
List<BakedQuad> quads = base.getQuads(null, null, ApiUtils.RANDOM_SOURCE, ModelData.EMPTY, null);
return split(quads, parts, transform);
});
WEAK_INSTANCES.add(this);
}
这里可能看起来有点复杂,主要是用了一点懒加载的机制,我们不关心这个,主要说的其实就是将对应的base拿到的getQuads顶点信息通过split方法进行了切分,这个切分就是看你的模型有多少个方块进行的。最后split返回一个Map,这个Map的key是相对位置,value是对应的渲染顶点信息。
@Nonnull
@Override
public List<BakedQuad> getQuads(
@Nullable BlockState state, @Nullable Direction side, @Nonnull RandomSource rand,
@Nonnull ModelData extraData, @Nullable RenderType layer
)
{
BlockPos offset = extraData.get(Model.SUBMODEL_OFFSET);//检查extraData中是否存在子模型偏移。
if(offset!=null)
return splitModels.get().getOrDefault(offset, ImmutableList.of());//如果存在偏移,则从缓存的splitModels中返回相应的四边形。
else
return base.getQuads(state, side, rand, extraData, layer);//如果不存在偏移,则从基础模型返回四边形。
}
而获得对应的顶点信息时候,需要传入一个额外的数据就是extraData中的SUBMODEL_OFFSET字段这个是方块相对机器的位置,通过这个位置我们就可以得到相对的位置的顶点信息,在map(splitModels)查询返回即可。
好了到这里我们说了模型信息是怎么处理的,大家应该也能明白怎么回事了,至于怎么切割的模型,请大家自己看split方法吧。如果不明白怎么传递的offset以及原版模型是怎么渲染出来的,那么你可以看看IBakedModel(烘培模型) - Boson 1.16和烘焙模型 - Flandre923并自己去手动实现下,去了解下这个大概的流程。然后在回来看这里的内容。