- Версия(и) Minecraft
- 1.12.2
Содержание
1 часть - Что такое ICustomModelLoader и как майнкрафт загружает модели.2 часть - Подробное описание интерфейсов для создания моделей.
3 часть - Создание предмета с плоской моделью.
4 часть - Создание блока с простой и сложной моделями.
В новой версии единственный способ загружать модели для обычных блоков и предметов -
использовать
ICustomModelLoader
. Об этом классе в этом уроке и пойдет речь.ЧАСТЬ I
Стандартный
VanillaLoader
принимает все блоки и предметы, загружает модель из json-файла. Он ищет модели в самую последнюю очередь.Интерфейс
ICustomModelLoader
содержит в себе 2 метода: accepts
и loadModel
.Первый метод определяет, будет ли данная моделька(параметр
modelLocation
) загружена текущим лоадером. Второй метод уже подгружает модельку.По логике вещей сначала
ResourceLocation
модельки пробегается по accept'ам всех зарегистрированных загрузчиков моделей, если какой-то лоадер его цепляет - уже вызывается метод loadModel
. В конце концов модель зацепит VanillaLoader
(см выше).loadModel
, как сказано выше, загружает: возвращает IModel
- необработанную модельку.ЧАСТЬ II
- Подробное описание интерфейсов для создания моделей.Класс
IModel
содержит в себе много методов:getDependencies - возвращает коллекцию зависимостей моделек(
Collection<ResourceLocation>
). Т.е. данная моделька будет 'выпечена'(об эFтом ниже) только после всех моделей-зависимостей.getTextures - возвращает коллекцию текстур, используемых этой моделькой. Данный метод необязательно переопределять, но желательно - текстурки которые вы будете использовать в модельке все равно загрузятся, просто в другое время.
getDefaultState - возвращает
IModelState
(трансформацию модельки) об этом ниже, в большинстве случаев можно оставить стандартную реализацию.getClip - возвращает некий
IClip
(Optional<? extends IClip>
). Этот вспомогательный интерфейс по большей части нужен для анимации и сглаживания(см net.minecraftforge.common.model.animation.Clips
- класс с множеством вариантов реализациий IClip
)smoothLighting - возвращает модельку(
IModel
) которая будет отображаться при включенном(параметр value
) ambient occlusion. В большинстве случаев рекомендуется оставить реализацию по умолчанию.uvlock - возвращает модельку(
IModel
), при вращении которой текстуры, применяемые к модели, не вращаются(при параметре value
== true
) вместе с ней. В большинстве случаев рекомендуется оставить реализацию по умолчанию.gui3d - возвращает модельку(
IModel
) которая будет отображаться в слоте инвентаря. (При значении параметра value
== false
модель должна быть плоской).process - возвращает модельку(
IModel
), с пользовательскими данными. Параметр ImmutableMap<String, String> customData
- пользовательские данные полученные из json-файла модельки, в виде ключ:значение. Вызывается только если моделька имеет json-файл blockstate.Далее 2 самых важных метода, переопределение которых рекомендуется:
retexture - возвращает модельку(
IModel
), которая будет загружена после ретекстурирования(не путать с перезагрузкой текстур F3+T
!). Метод должен возвращать новую модельку с новыми текстурами, старые должны быть автоматически удалены. Параметр ImmutableMap<String, String> textures
- старые текстурки в строковом формате.bake - основной метод
IModel
. По большому счету можно реализовать только его. 'Выпекает' (возвращает) IBakedModel
- готовую к рендеру модельку. Это происходит при запуске майнкрафта или при перезагрузке ресурсов(F3+T
). Параметр state
- это изначальная трансформация(IModelState
) полученная из getDefaultState
(см выше), format
- формат вершин(VertexFormat
), bakedTextureGetter
- функция(Function<ResourceLocation, TextureAtlasSprite>
), с помощью которой можно из ResourceLocation
получать TextureAtlasSprite
. Пример: bakedTextureGetter.apply(loc);
где loc
- путь до текстуры, bakedTextureGetter
- данная функция.Разберем методы интерфейса
IBakedModel
:isAmbientOcclusion - возвращает
boolean
, определяет можно ли к данной модельке применить AmbientOcclusion.isGui3d - возвращает
boolean
, определяет, будет ли данная модель рендерится плоской в слоте инвентаря или в виде выброшенного предмета (EntityItem
).isBuiltInRenderer - возвращает
boolean
, при true, текущая модель отрисовываться не будет, вместо неё будет рендерится TileEntityItemStackRenderer
. Этот метод используют такие ванильные блоки как сундук или флаг(баннер).getOverrides - возвращает
ItemOverrideList
, с помощью него можно удобно создавать отдельно модельку предмету, отдельно блоку(см ниже).getParticleTexture - возвращает текстуру частиц(
TextureAtlasSprite
), используется при создании частиц данной модельки. Например для предмета такие частицы появляются при съедании, для блока при ходьбе по нему.handlePerspective - возвращает готовую модельку матрицу перспективы 4x4(
Pair<? extends IBakedModel, Matrix4f>
, очень мощный метод для трансформации модельки, напрямую лучше не реализовывать. Параметр cameraTransformType - тип текущей трансформации(см net.minecraft.client.renderer.block.model.ItemCameraTransforms.TransformType
)getItemCameraTransforms - возвращает трансформацию модели предмета(
ItemCameraTransforms
). Вызывается каждый кадр, когда рендерится предмет.getQuads - основной метод данного интерфейса
IBakedModel
, возвращает список 'запеченных' квадратов(List<BakedQuad>
), которые впоследствии передаются на графический процессор, где они уже будут отрисованы opengl'ем. Примечание: У блоков этот метод вызывается не каждый кадр, а только при обновлении рендера блока. Обновление рендера блока происходит либо когда блок впервые попал в поле зрения камеры, или когда рядом с ним обновился какой-либо blockstate. У предметов данный метод вызывается каждый кадр.ЧАСТЬ III
Теперь немного практики. (УРА!)
Первым делом создадим свой
ICustomModelLoader
Java:
public class TestCustomModelLoader implements ICustomModelLoader {
@Override
public boolean accepts(ResourceLocation modelLocation) {
return false;
}
@Override
public IModel loadModel(ResourceLocation modelLocation) throws Exception {
return null;
}
@Override
public void onResourceManagerReload(IResourceManager resourceManager) {
}
}
Делать это нужно в специальном событии, на клиентской стороне разумеется.
Java:
@SubscribeEvent
public void eventRegisterModel(ModelRegistryEvent event){
ModelLoaderRegistry.registerLoader(new TestCustomModelLoader());
}
Для примера создадим предмет. Я буду делать алмазный кинжал.
Java:
static Item testItem;
Java:
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
testItem = new Item();
testItem.setRegistryName("testitem");
ForgeRegistries.ITEMS.register(testItem);
}
ModelResourceLocation
:
Java:
Minecraft.getMinecraft().getRenderItem().getItemModelMesher().register(testItem,
0, new ModelResourceLocation(testItem.getRegistryName(),
"inventory"));
Теперь сделаем так, чтобы наш
ICustomModelLoader
загрузил модель именно для этого предмета.Переопределим метод accepts.
Java:
@Override
public boolean accepts(ResourceLocation modelLocation) {
/*
* напрямую modelLocation'ы лучше не сравнивать, т.к.
* в процессе загрузки моделей они несколько раз пересоздаются
*/
return modelLocation.toString().contains("testitem");
}
Java:
public class TestItemModel implements IModel {
final ResourceLocation texture;
public TestItemModel(ResourceLocation texture){
this.texture = texture;
}
//Добавляем текстурки которые будем использовать
@Override
public Collection<ResourceLocation> getTextures() {
return Lists.newArrayList(texture);
}
//Строим саму модель
@Override
public IBakedModel bake(IModelState state, VertexFormat format, Function<ResourceLocation, TextureAtlasSprite> bakedTextureGetter) {
//получаем TextureAtlasSprite из ResourceLocation
TextureAtlasSprite textureAtlasSprite = bakedTextureGetter.apply(texture);
//Получаем список квадратов из текструки, для этого уже есть удобный метод.
ImmutableList<BakedQuad> quads = ItemLayerModel.getQuadsForSprite(0, textureAtlasSprite, format, state.apply(Optional.empty()));
return new TestItemBakedModel(quads, textureAtlasSprite);
}
}
Java:
public class TestItemBakedModel implements IBakedModel {
List<BakedQuad> quads;
TextureAtlasSprite textureAtlasSprite;
public TestItemBakedModel(List<BakedQuad> quads, TextureAtlasSprite textureAtlasSprite){
this.quads = quads;
this.textureAtlasSprite = textureAtlasSprite;
}
//возвращаем квадраты для рендера
@Override
public List<BakedQuad> getQuads(@Nullable IBlockState state, @Nullable EnumFacing side, long rand) {
return quads;
}
//Пусть на нашу модельку действуем мягкое освещение
@Override
public boolean isAmbientOcclusion() {
return false;
}
//Делаем модельку плоской в GUI
@Override
public boolean isGui3d() {
return false;
}
//У нашего предмета нет TileEntityItemStackRenderer
@Override
public boolean isBuiltInRenderer() {
return false;
}
//Текстура частиц пусть будет такой же как и текстура предмета
@Override
public TextureAtlasSprite getParticleTexture() {
return textureAtlasSprite;
}
//Стандартный лист дефолтных трансформаций
@Override
public ItemOverrideList getOverrides() {
return ItemOverrideList.NONE;
}
}
ICustomModelLoader
:
Java:
@Override
public IModel loadModel(ResourceLocation modelLocation) throws Exception {
//эту проверку можно ибрать если одним лоадером прогружается лишь одна модель, или несколько одинаковых
if(modelLocation.toString().contains("testitem"))
return new TestItemModel(/* Указываем путь до текстурки предмета */ new ResourceLocation("items/diamond_sword"));
return null;
}
Ура, получилось!
Подождите-ка...
Модель отображается неправильно...
Добавим в нашу испеченную модель такой код, трансформацию для кинжала:
Java:
//Трансформация предмета когда он выброшен в мире(EntityItem)
ItemTransformVec3f entity = new ItemTransformVec3f(/*Поворот по осям X Y Z*/new Vector3f(0f,0,0), new Vector3f(/*Смещение по осям X Y Z*/0.005f, 0.15f, 0.04f), new Vector3f(/*Масштаб по осям X Y Z*/0.55f, 0.55f, 0.55f));
//От третьего лица в правой руке
ItemTransformVec3f thirdPersonRight = new ItemTransformVec3f(new Vector3f(0f,-90,-160), new Vector3f(0.005f, -0.35f, 0.04f), new Vector3f(0.55f, 0.55f, 0.55f));
//От третьего лица в левой руке
ItemTransformVec3f thirdPersonLeft = new ItemTransformVec3f(new Vector3f(0f,-90,150), new Vector3f(0.005f, -0.40f, 0.1f), new Vector3f(0.55f, 0.55f, 0.55f));
//От первого лица в правой руке
ItemTransformVec3f firstPersonRight = new ItemTransformVec3f(new Vector3f(-140,-90,25), new Vector3f(0.03f, 0.23f, 0.104f), new Vector3f(0.68f, 0.68f, 0.68f));
//От первого лица в левой руке
ItemTransformVec3f firstPersonLeft = new ItemTransformVec3f(new Vector3f(-120,-90,25), new Vector3f(0.03f, 0.08f, 0.104f), new Vector3f(0.68f, 0.68f, 0.68f));
//Общая трансформация. Вместо некоторый отдельных ItemTransformVec3f, мы добавляем дефолтные, тк они не будут использованы обычным предметом
ItemCameraTransforms itemCameraTransforms = new ItemCameraTransforms(thirdPersonLeft, thirdPersonRight, firstPersonLeft, firstPersonRight, ItemTransformVec3f.DEFAULT, ItemTransformVec3f.DEFAULT, entity, ItemTransformVec3f.DEFAULT);
@Override
public ItemCameraTransforms getItemCameraTransforms() {
return itemCameraTransforms;
}
ЧАСТЬ IV
- Создание блока с простой и сложной моделями.Для этой части нужна специальная 'обертка стандартных функций', скачать ее можно в архиве, который прикреплен к этому уроку. Переместите данные из архива в исходники вашего мода(папка src/main/java).
(Все используемые ниже классы есть в библиотеке выше)
Разберем библиотеку по кусочку.
Первый класс - ModelBaker
Позволяет создавать коллекции 'запеченных' квадратов.
Имеет 1 статический экземпляр, с помощью которого можно вызывать основные методы, все из которых кроме
bake
возвращают тот самый статический экземпляр.Методы:
begin - начинает создание моделей
setTexture - устанавливает текстуру
putQuad - добавляет квадрат в локальный буфер
putCube - добавляет 6 квадратов(куб) в локальный буффер
putTexturedCube - добавляет куб в локальный буффер с uv равным положению текстурки, которую мы добавили в
setTexture
bake - 'запекает' квадраты из локального буффера в коллекцию
List<BakedQuad>
Чтобы создать модельку, надо:
1) начать 'выпечку'
begin
2) установить текстуру
setTexture
3) поместить произвольное кол-во квадратов(или кубов) в локальный буфер
putQuad
4) 'выпечь' модельку
bake
Второй класс - QuadBuilder
Позволяет создавать
BakedQuad
с текстурой.Имеет 1 статический экземпляр, с помощью которого можно вызывать основные методы, все из которых кроме
build
возвращают тот самый статический экземпляр.Методы:
begin - начинает создание квадрата
setTexture - устанавливает текстуру
putVertex - добавляет вершину в локальных буфер
tex - устанавливает uv для будущих вершин
build - собирает вершины из локального буфера в
BakedQuad
Чтобы создать квадрат, надо:
1) начать сборку
begin
2) установить текстуру
setTexture
3) установить uv
tex
4) поместить 4 вершины в локальный буфер
putVertex
5) 'построить' модельку
build
Остальные классы вспомогательные, ничего касающегося данного урока в них нет.
Теперь попробуем создать модель для блока!
Java:
static Block testBlock;
Java:
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
testBlock = new Block(Material.IRON);
testBlock.setRegistryName("testname");
ForgeRegistries.BLOCKS.register(testBlock);
ForgeRegistries.ITEMS.register(new ItemBlock(testBlock).setRegistryName(testBlock.getRegistryName()));
}
ModelResourceLocation
итемблока:
Java:
Minecraft.getMinecraft().getRenderItem().getItemModelMesher().register(Item.getItemFromBlock(testBlock),
0, new ModelResourceLocation(testBlock.getRegistryName(),
"inventory"));
Java:
public class BlockModel implements IModel{
final ResourceLocation loc;
public BlockModel(ResourceLocation loc){
this.loc = loc;
}
@Override
public IBakedModel bake(IModelState state, VertexFormat format, Function<ResourceLocation, TextureAtlasSprite> bakedTextureGetter) {
//текстурка
TextureAtlasSprite sprite = bakedTextureGetter.apply(loc);
//Получаем экземпляр для лучшей читабельности
ModelBaker baker = ModelBaker.INSTANCE;
//начинаем создание модели
baker.begin(state, format);
//привязываем текстурку
baker.setTexture(sprite);
//создаем куб размером 0.5 - это полная величина, т.е. куб будет размером с полноценный блок(1 метр)
baker.putTexturedCube(0,0,0,0.5f);
return new BlockBakedModel(/*Испекаем нашу модель*/baker.bake(), bakedTextureGetter.apply(loc), format, state);
}
@Override
public Collection<ResourceLocation> getTextures() {
return Lists.newArrayList(loc);
}
}
Java:
public class BlockBakedModel implements IBakedModel {
List<BakedQuad> bakedQuads;
TextureAtlasSprite textureParticle;
VertexFormat format;
IModelState state;
public BlockBakedModel(List<BakedQuad> bakedQuads, TextureAtlasSprite textureParticle, VertexFormat format, IModelState state){
this.bakedQuads = bakedQuads;
this.textureParticle = textureParticle;
this.format = format;
this.state = state;
}
@Override
public List<BakedQuad> getQuads(@Nullable IBlockState state, @Nullable EnumFacing side, long rand) {
return bakedQuads;
}
@Override
public boolean isAmbientOcclusion() {
return true;
}
@Override
public boolean isGui3d() {
return true;
}
@Override
public boolean isBuiltInRenderer() {
return false;
}
@Override
public TextureAtlasSprite getParticleTexture() {
return textureParticle;
}
@Override
public ItemOverrideList getOverrides() {
return ItemOverrideList.NONE;
}
}
Java:
public class TestCustomModelLoader implements ICustomModelLoader {
@Override
public boolean accepts(ResourceLocation modelLocation) {
/*
* напрямую modelLocation'ы лучше не сравнивать, т.к.
* в процессе загрузки моделей они несколько раз пересоздаются
*/
return modelLocation.toString().contains("testitem") || modelLocation.toString().contains("testname");
}
@Override
public IModel loadModel(ResourceLocation modelLocation) throws Exception {
if(modelLocation.toString().contains("testitem")) {
//весь предыдущий код в туториале можно заменить на стандартный класс
ImmutableList.Builder<ResourceLocation> list = ImmutableList.builder();
list.add(new ResourceLocation("items/diamond_sword"));
return new ItemLayerModel(list.build());
}
else if(modelLocation.toString().contains("testname")){
return new BlockModel(new ResourceLocation(/*Путь до текстуры блока*/"blocks/enchanting_table_top"));
}
return null;
}
@Override
public void onResourceManagerReload(IResourceManager resourceManager) {
}
}
Красота
Но с моделькой итемблока предмета все совсем плохо
Она громадная, загораживает четверть экрана.
К счастью эту проблему можно легко решить в пять строчек благодаря
ItemOverrideList
(см выше)Переопределим
getOverrides
в IBakedModel
нашего блока:
Java:
@Override
public ItemOverrideList getOverrides() {
return ItemOverrideListBlock.INSTANCE;
}
ItemOverrideListBlock
:
Java:
class ItemOverrideListBlock extends ItemOverrideList{
public static ItemOverrideListBlock INSTANCE = new ItemOverrideListBlock(new ArrayList<>());
IBakedModel itemModel;
public ItemOverrideListBlock(List<ItemOverride> overridesIn) {
super(overridesIn);
}
@Override
public IBakedModel handleItemState(IBakedModel originalModel, ItemStack stack, World world, EntityLivingBase entity) {
if(itemModel != null) return itemModel;
if(originalModel instanceof BlockBakedModel == false) return originalModel;
BlockBakedModel original = (BlockBakedModel) originalModel;
ModelBaker baker = ModelBaker.INSTANCE;
baker.begin(original.state, original.format);
TextureAtlasSprite sprite = original.textureParticle;
baker.setTexture(sprite);
//Делаем модельку в 2 раза меньше оригинальной
baker.putTexturedCube(0, 0, 0, 0.25f);
itemModel = new BlockBakedModel(baker.bake(), original.textureParticle, original.format, original.state);
return itemModel;
}
}
handleItemState
(как и getOverrides
) в классе ItemOverrideList
вызывается каждый кадр -> Он должен быть быстрым, для этого нужно кэшировать уменьшенную IBakedModel
(как в примере выше).Все, можно запускать.
Выглядит ничем не хуже обычного, так даже интереснее смотрится!
Но все равно, это выглядит немного не так как обычный блок. Уберем выше сделанный ItemOverrideList у нашего блока, добавим трансформацию в ibakedmodel.
Java:
//Копируем все трансформации из block/generated.json
//Трансформация от первого лица в правой руке
ItemTransformVec3f firstperson_righthand = new ItemTransformVec3f(new Vector3f(0,45,0), new Vector3f(0, 0, 0), new Vector3f(0.40f, 0.40f, 0.40f));
//От первого лица в левой руке
ItemTransformVec3f firstperson_lefthand = new ItemTransformVec3f(new Vector3f(0,225,0), new Vector3f(0, 0, 0), new Vector3f(0.40f, 0.40f, 0.40f));
//От третьего лица
ItemTransformVec3f thirdperson_righthand = new ItemTransformVec3f(new Vector3f(75,45,0), new Vector3f(0, 0.15f, 0), new Vector3f(0.375f, 0.375f, 0.375f));
//На голове
ItemTransformVec3f fixed = new ItemTransformVec3f(new Vector3f(0,0,0), new Vector3f(0, 0, 0), new Vector3f(0.5f, 0.5f, 0.5f));
//В виде предмета в мире(EntityItem)
ItemTransformVec3f ground = new ItemTransformVec3f(new Vector3f(0,0,0), new Vector3f(0, 0.15f, 0), new Vector3f(0.25f, 0.25f, 0.25f));
//В графическом интерфейсе
ItemTransformVec3f gui = new ItemTransformVec3f(new Vector3f(30,225,0), new Vector3f(0, 0, 0), new Vector3f(0.625f, 0.625f, 0.625f));
//Общая транфсормация
ItemCameraTransforms itemCameraTransforms = new ItemCameraTransforms(thirdperson_righthand, thirdperson_righthand, firstperson_lefthand, firstperson_righthand, ItemTransformVec3f.DEFAULT, gui, ground, fixed);
@Override
public ItemCameraTransforms getItemCameraTransforms() {
return itemCameraTransforms;
}
Давайте теперь создадим не просто куб, а много кубов в одной модельке. С моим апи это очень просто:
Java:
@Override
public IBakedModel bake(IModelState state, VertexFormat format, Function<ResourceLocation, TextureAtlasSprite> bakedTextureGetter) {
TextureAtlasSprite sprite = bakedTextureGetter.apply(loc);
TextureAtlasSprite spriteBooks = bakedTextureGetter.apply(new ResourceLocation("blocks/bookshelf"));
ModelBaker baker = ModelBaker.INSTANCE;
baker.begin(state, format);
baker.setTexture(sprite);
baker.putTexturedCube(0, 0.25f, 0, 0.25f);
baker.setTexture(spriteBooks);
baker.putCube(/*offsetX*/ 0f,/*offsetY*/ -0.25f,/*offsetZ*/ 0f,
/*scaleX*/0.5f, /*scaleY*/0.25f,/*scaleZ*/ 0.5f,
spriteBooks.getMinU(), spriteBooks.getMaxU(),spriteBooks.getMinV(),spriteBooks.getMaxV());
return new BlockBakedModel(/*Испекаем нашу модель*/baker.bake(), bakedTextureGetter.apply(loc), format, state);
}
ModelBaker
есть удобный метод для создания квадратов.Спасибо за внимание.