Flandre923
1521 words
8 minutes
沉浸工艺多方块04-多方块模型的实现

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并自己去手动实现下,去了解下这个大概的流程。然后在回来看这里的内容。

沉浸工艺多方块04-多方块模型的实现
https://fuwari.vercel.app/posts/imm/04-多方块模型的实现/
Author
Flandre923
Published at
2024-08-10