Добавление моделей блокам и предметам

Добавление моделей блокам и предметам

Нет прав для скачивания
Версия(и) Minecraft
1.12.2
Содержание
1 часть - Что такое ICustomModelLoader и как майнкрафт загружает модели.
2 часть - Подробное описание интерфейсов для создания моделей.

3 часть - Создание предмета с плоской моделью.
4 часть - Создание блока с простой и сложной моделями.



В новой версии единственный способ загружать модели для обычных блоков и предметов -
использовать ICustomModelLoader. Об этом классе в этом уроке и пойдет речь.



ЧАСТЬ I
- Что такое ICustomModelLoader и как майнкрафт загружает модели.

Стандартный 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;
}
Все готово, теперь можно запускать.
1533925587789.png

Ура, получилось!

Подождите-ка...
1533925623254.png

Модель отображается неправильно...
Добавим в нашу испеченную модель такой код, трансформацию для кинжала:
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;
}
Теперь у нас модель отображается нормально.
2018-08-10_21.30.57.png


ЧАСТЬ 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) {

    }
}
Все готово, можно запускать.

Красота
2018-06-14_21.41.12.png

Но с моделькой итемблока предмета все совсем плохо
2018-06-14_21.41.59.png

Она громадная, загораживает четверть экрана.
К счастью эту проблему можно легко решить в пять строчек благодаря 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(как в примере выше).
Все, можно запускать.
2018-06-14_21.57.14.png

Выглядит ничем не хуже обычного, так даже интереснее смотрится!
Но все равно, это выглядит немного не так как обычный блок. Уберем выше сделанный 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;
    }
Теперь блок в инвентаре выглядит нормально.
Давайте теперь создадим не просто куб, а много кубов в одной модельке. С моим апи это очень просто:
2018-06-14_22.06.22.png

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 есть удобный метод для создания квадратов.
Спасибо за внимание.
Автор
Могучий горгон
Скачивания
60
Просмотры
9,471
Первый выпуск
Обновление
Оценка
4.33 звёзд 6 оценок

Другие ресурсы пользователя Могучий горгон

Последние обновления

  1. Больше информации, и исправление недочетов

    Добавлено описание 'вспомогательной обертки ванильных функций'. Разбил туториал на 4 части...
  2. 2 часть.

    Поправил баги, изменил немного текст. Добавлено больше информации.

Последние рецензии

Ооо, я сэкономил целые часы времени, спасибо!
А ведь, искал на фордж форуме, столько времени зря тратил...
Для меня это сложно, но ты всё очень хорошо разжевал, пригодится много кому. Так держать!
Супер, а главное на Java.
Модель от первого лица так и не исправил, а твоё "лучше чем ничего" звучит как "мне лень разбираться, сами как-нибудь"
Столько воды чуть не утонул.
- полностью расписана ванильная регистрация предмета (ладн, пусть будет)
- где ты "исправил меч" и под скрином написал "лучше чем ничего" видны огромные косяки - в хотбаре рендерится черт знает как - рендер от первого лица тоже кривой, не по стандарту.
- Написал бы больше пунктов по делу, но в гайд не вчитывался т.к. не пишу на 1.12
- ошибки написания туториала, удивлен, что за них гарик поставил тебе 5 звезд, не учитывая их, а мне 3 не указав на них, дает знак.
Про рендер модели айтема от третьего лица вообще умолчал...
Невероятно полезная штука для создания моделей. В отличий от туториала @hohserg"а здесь рассказано про блоки и так же это всё написано на java!
Сверху