IBakedModel(烘焙模型)

首先我们从IBakedModel开始讲起。在开始正式写代码之前,我们先要了解如何一个方块是如何渲染出来的

Minecraft本身会读取json和材质文件将其转换成IModel接口的实例,然后IModel进行了「Bake(烘焙)」处理,变成了IBakedModel,这个IBakedModel会被放入BlockRendererDispatcher中,当游戏需要时会直接从BlockRendererDispatcher取出IBakedModel进行渲染。至于什么是「Bake」,Bake基本上是对模型的材质进行光照计算等操作,让它变成可以直接被GPU渲染的东西。

在这节中我们将来研究如何为我们的方块自定义IBakedModel。这节我们将要制作一个「隐藏方块」,这个方块会自动的显示它下方方块的模型的材质(虽然有些Bug,但是为了演示这已经足够了)。

首先是方块ObsidianHiddenBlock

public class ObsidianHiddenBlock extends Block {
    public ObsidianHiddenBlock() {
        super(Properties.create(Material.ROCK).hardnessAndResistance(5).notSolid());
    }
}

内容非常的简单,相信大家都看得懂。

然后就是关键所在:ObsidianHiddenBlockModel:

public class ObsidianHiddenBlockModel implements IBakedModel {
    IBakedModel defaultModel;
    public static ModelProperty<BlockState> COPIED_BLOCK = new ModelProperty<>();

    public ObsidianHiddenBlockModel(IBakedModel existingModel) {
        defaultModel = existingModel;
    }

    @Nonnull
    @Override
    public List<BakedQuad> getQuads(@Nullable BlockState state, @Nullable Direction side, @Nonnull Random rand, @Nonnull IModelData extraData) {
        IBakedModel renderModel = defaultModel;
        if (extraData.hasProperty(COPIED_BLOCK)) {
            BlockState copiedBlock = extraData.getData(COPIED_BLOCK);
            if (copiedBlock != null) {
                Minecraft mc = Minecraft.getInstance();
                BlockRendererDispatcher blockRendererDispatcher = mc.getBlockRendererDispatcher();
                renderModel = blockRendererDispatcher.getModelForState(copiedBlock);
            }
        }
        return renderModel.getQuads(state, side, rand, extraData);
    }

    @Override
    public IModelData getModelData(IBlockDisplayReader world, BlockPos pos, BlockState state, IModelData tileData) {
        BlockState downBlockState = world.getBlockState(pos.down());
        ModelDataMap modelDataMap = new ModelDataMap.Builder().withInitial(COPIED_BLOCK, null).build();

        if (downBlockState.getBlock() == Blocks.AIR || downBlockState.getBlock() == BlockRegistry.obsidianHidden.get()) {
            return modelDataMap;
        }
        modelDataMap.setData(COPIED_BLOCK, downBlockState);
        return modelDataMap;
    }

    @Override
    public List<BakedQuad> getQuads(@Nullable BlockState state, @Nullable Direction side, Random rand) {
        throw new AssertionError("IBakedModel::getQuads should never be called, only IForgeBakedModel::getQuads");
    }

    @Override
    public boolean isAmbientOcclusion() {
        return defaultModel.isAmbientOcclusion();
    }

    @Override
    public boolean isGui3d() {
        return defaultModel.isGui3d();
    }

    @Override
    public boolean isSideLit() {
        return defaultModel.isSideLit();
    }

    @Override
    public boolean isBuiltInRenderer() {
        return defaultModel.isBuiltInRenderer();
    }

    @Override
    public TextureAtlasSprite getParticleTexture() {
        return defaultModel.getParticleTexture();
    }

    @Override
    public ItemCameraTransforms getItemCameraTransforms() {
        return null;
    }

    @Override
    public ItemOverrideList getOverrides() {
        return defaultModel.getOverrides();
    }
}

首先是构造函数:

public ObsidianHiddenBlockModel(IBakedModel existingModel) {
  defaultModel = existingModel;
}

可以看到,这部分的代码非常的长,但是其实没有你想象的那么复杂,我们一一来讲解。

可以看到这个构造函数传入了一个IBakedModel,这个传入的IBakedModel就是我们方块默认的模型,因为我们希望我们的方法当放置在半空中时,可以显示默认的模型,所以我们需要保留一份默认的模型。

然后就是最后面的六个方法,作用如下。

函数名作用
isAmbientOcclusion控制是否开启环境光遮蔽
isGui3d控制掉落物是否是3D的
isSideLit()暂不明,应该和物品的渲染光有关
isBuiltInRenderer是否使用内置的渲染,返回Ture会使用ISTR渲染
getParticleTexture粒子效果材质
getOverrides获取模型的复写列表

在这里我们直接调用了默认模型的相关方法,就不需要自己设置了。

接下了我们来讲解最为重要的两个方法getQuadsgetModelData,请注意这里面有两个同名的getQuads方法,但是参数值不同,其中有3个参数的getQuads,是必须要实现的,但是我们不会调用它,因为它没法传入来提供渲染,所以我们直接写了一个异常,来告知我们这个方法被错误的调用。

其中getQuadsgetModelData的关系和作用是,getQuadsIBakedModel的核心方法,它将返回一堆Quads,正如Quad这个词就如同它的字面意义那样,一个由四条边组成的形状。如果你对建模有所了解,你应该知道,任何和3D图形其实都是可以用三角形拼成的,在Minecraft里,任何的模型都是用Quad拼成的。对于一个普通的方块来说,它需要6个Quads,对于一些有着特殊模型的方块,需要的Quad数会更多。

当Minecraft开始渲染IBakedModel里,它就会调用这个方法获取Quads,然后渲染这些Quads

接下来是getModelData方法,它的作用是给getQuads方法传入额外的数据,getQuads方法有一个IModelData extraData参数,这里的extraData就是通过getModelData传入的。IModelData的数值也是以「键值对」对信息储存的。

public static ModelProperty<BlockState> COPIED_BLOCK = new ModelProperty<>();

COPIED_BLOCK就是我们声明的一个「键」,可以看到他的类型是ModelProperty<BlockState>,这意味着,相对应「键值对」里「值」对类型是BlockState

然后我们来看getModelData方法的具体内容:

@Override
public IModelData getModelData(IBlockDisplayReader world, BlockPos pos, BlockState state, IModelData tileData) {
  BlockState downBlockState = world.getBlockState(pos.down());
  ModelDataMap modelDataMap = new ModelDataMap.Builder().withInitial(COPIED_BLOCK, null).build();

  if (downBlockState.getBlock() == Blocks.AIR || downBlockState.getBlock() == BlockRegistry.obsidianHidden.get()) {
    return modelDataMap;
  }
  modelDataMap.setData(COPIED_BLOCK, downBlockState);
  return modelDataMap;
}
 BlockState downBlockState = world.getBlockState(pos.down());

首先我们获取了「隐藏方块」下方方块的BlockState

ModelDataMap modelDataMap = new ModelDataMap.Builder().withInitial(COPIED_BLOCK, null).build()

ModelDataMapIModelData接口的唯二两个实现类中的一个,我们这里创建了一个键值对,并且通过withInitial设置了初始值:「键:COPIED_BLOCK,值:null」。

if (downBlockState.getBlock() == Blocks.AIR || downBlockState.getBlock() == BlockRegistry.obsidianHidden.get()) {
    return modelDataMap;
}

然后我们判断这个BlockState是不是空气,以及是不是又是一个相同的「隐藏方块」,如是就直接返回ModelDataMap

如果不是

modelDataMap.setData(COPIED_BLOCK, downBlockState);

通过调用setData方法设置了具体的「值」,然后返回。

怎么样这个逻辑不是很难理解吧。

接下去就是核心方法getQuads:

@Nonnull
@Override
public List<BakedQuad> getQuads(@Nullable BlockState state, @Nullable Direction side, @Nonnull Random rand, @Nonnull IModelData extraData) {
  IBakedModel renderModel = defaultModel;
  if (extraData.hasProperty(COPIED_BLOCK)) {
    BlockState copiedBlock = extraData.getData(COPIED_BLOCK);
    if (copiedBlock != null) {
      Minecraft mc = Minecraft.getInstance();
      BlockRendererDispatcher blockRendererDispatcher = mc.getBlockRendererDispatcher();
      renderModel = blockRendererDispatcher.getModelForState(copiedBlock);
    }
  }
  return renderModel.getQuads(state, side, rand, extraData);
}

这里的逻辑其实也非常简单。

IBakedModel renderModel = defaultModel;

首先设置了默认的渲染模型。

if (extraData.hasProperty(COPIED_BLOCK))

然后判断传入的数据有没有COPIED_BLOCK这个键。

BlockState copiedBlock = extraData.getData(COPIED_BLOCK);

获取COPIED_BLOCK这个键,相对应的值。

if (copiedBlock != null)

如果值不为null

Minecraft mc = Minecraft.getInstance();
BlockRendererDispatcher blockRendererDispatcher = mc.getBlockRendererDispatcher();
renderModel = blockRendererDispatcher.getModelForState(copiedBlock);

就从Minecraft的getBlockRendererDispatcher,取出对应BlockState的模型,放入renderModel中。

return renderModel.getQuads(state, side, rand, extraData);

最后向下调用renderModel进行渲染,因为调用的IBakedModel是Minecraft实现的,所以我们不必去思考到底是怎么渲染的,有兴趣的同学可以自行研究。

以上如此,我们自定义的IBakedMode已经创建完毕。

Minecraft 在默认情况下会给方块自动创建一个IBakeModel,我们需要替换自动创建的IBakeModel,幸运的是Forge提供给我们了一个事件来实现这个功能。

ModBusEventHandler.java:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD,value = Dist.CLIENT)
public class ModBusEventHandler {
    @SubscribeEvent
    public static void onModelBaked(ModelBakeEvent event) {
        for (BlockState blockstate : BlockRegistry.obsidianHidden.get().getStateContainer().getValidStates()) {
            ModelResourceLocation modelResourceLocation = BlockModelShapes.getModelLocation(blockstate);
            IBakedModel existingModel = event.getModelRegistry().get(modelResourceLocation);
            if (existingModel == null) {
                throw new RuntimeException("Did not find Obsidian Hidden in registry");
            } else if (existingModel instanceof ObsidianHiddenBlockModel) {
                throw new RuntimeException("Tried to replaceObsidian Hidden twice");
            } else {
                ObsidianHiddenBlockModel obsidianHiddenBlockModel = new ObsidianHiddenBlockModel(existingModel);
                event.getModelRegistry().put(modelResourceLocation, obsidianHiddenBlockModel);
            }
        }
    }
}

请注意替换IBakedModel是在游戏启动过程替换的,所以我们这里使用的是Mod.EventBusSubscriber.Bus.MOD,还有请注意,我们同样不希望它在物理服务器上加载,所以加上了value = Dist.CLIENT来确保他只在物理客户端上加载。

@SubscribeEvent
public static void onModelBaked(ModelBakeEvent event)

我们监听了ModelBakeEvent事件。

接下去的逻辑基本上就是获取我们方块对应的所有State,因为每一个不同的方块状态都可能对应着一个不同的模型(虽然我们的方块没有方块状态,但是这个还是要做的)。然后通过event.getModelRegistry().get方法从方块状态中获取默认的IBakedModel,创建了一个我们自己的ObsidianHiddenBlockModel,然后用event.getModelRegistry().put替换了进去。

接下了就是常规的注册方块和物品了。

另外我们的方块的状态文件如下:

{
  "variants": {
    "": { "model": "boson:block/obsidian_hidden" }
  }
}

可以看到并没有额外的方块状态,所以上面的循环只会运行一次。

打开游戏,你就可以看到我们创建的「隐藏方块」了。

image-20200430173122813

在这张图片里上面的方块都是同一种方块。

源代码地址