Создание моделей при помощи кода

Создание моделей при помощи кода

Нет прав для скачивания
Версия(и) Minecraft
1.10+
Всем привет.
Вы уже знаете, что модели для предметов можно создавать при помощи json, в виде ресурсов.
В этом гайде будет рассмотрен альтернативный вариант создания моделей - при помощи кода.


Зачем оно?
На то есть две причины(как минимум).
  • Во первых, для того, чтобы создавать динамические модели. Json не позволяет делать движущиеся и зависящие от нбт полигоны.
  • Во вторых, json-ном нельзя сделать косых полигонов. А созданные кодом могут располагаться как вам угодно, под любыми углами.
  • В третьих, давайте посмотрим что вас ждет, если вы будете обмазываться json-моделями:
Ресурсы из Туманного мира Liahim:
1528033326164.png

И этот список продолжается еще дальше. Не много ли для таких довольно однотипных блоков?
1528033290852.png

При создании моделей кодом вы конечно же сможете повторно использовать код.


Основная идея

Нам доступно событие ModelBakeEvent, при помощи которого мы можем установить произвольную модель нашему предмету.

Мы можем создать наследников IBakedModel и IPerspectiveAwareModel, которые будут нашими моделями. В них интересны методы getOverrides: ItemOverrideList, getQuads: util.List[BakedQuad], handlePerspective

Первый нужен для динамических частей модели, второй возвращает статические.
Полигоны модели, представлены экземплярами BakedQuad — четырехвершинными полигонами.

А третий может задать матрицей изменение положения и размеров нашей модели в зависимости от того, где она находится: в инвентаре, валяется в мире, или игрок держит в руке. Имеет смысл выносить эти преобразования в отдельный метод, т.к. сами полигоны не меняются, меняется только положение и размер модели в целом


Ближе к делу

Мы будем создавать модель жезла из таумкрафт 4.
Возможно, вы подумали, что было бы здорово сделать блоки веток как в Туманном мире, но тему блоков(как и наклонных полигонов) я оставлю для одного из следующих уроков.
Должно получить че-то вроде:
1528040820997.png


Scala:
//Замена модели
@SubscribeEvent
def bakeModel(event: ModelBakeEvent): Unit = {
  //получаем текущую
  val existingModel = event.getModelRegistry.getObject(itemModelResourceLocation)
  existingModel match {
    case baseModel: IBakedModel =>
      //мы будем использовать json-модель в качестве базовой для некоторых плюшек ванильной
      //реализации handlePerspective, пока не задумываемся
      val customModel = new WandModel(baseModel)
      //устанавливаем текущую
      event.getModelRegistry.putObject(itemModelResourceLocation, customModel)
    case _ =>
      println("base model is missing, for",itemModelResourceLocation)
  }
}

Json пустой модели:
JSON:
{
    "parent": "item/generated"
}

Наша модель-обертка
Scala:
class WandModel(baseModel: IBakedModel) extends IPerspectiveAwareModel {

  override def getQuads(state: IBlockState, side: EnumFacing, rand: Long) = Collections.emptyList[BakedQuad]() //baseModel.getQuads(state, side, rand) //можно совмещать модель json и написанную кодом

  override def getOverrides: ItemOverrideList = CustomItemOverrideList//Это синглтон. Нам не нужно больше одного экземпляра ItemOverrideList

  override def handlePerspective(cameraTransformType: ItemCameraTransforms.TransformType): Pair[_ <: IBakedModel, Matrix4f] = {
  //Пока мы только заюзаем плюшки json-модели, вроде дерганья предмета при пкм и движении игрока
  baseModel match {
    case model: IPerspectiveAwareModel =>
      val matrix4f = model.handlePerspective(cameraTransformType).getRight
      Pair.of(this, matrix4f)
    case _ =>
      val itemCameraTransforms = baseModel.getItemCameraTransforms
      val itemTransformVec3f = itemCameraTransforms.getTransform(cameraTransformType)
      val mat: Matrix4f = Option(new TRSRTransformation(itemTransformVec3f).getMatrix).orNull
      Pair.of(this, mat)
  }
}

  //Не особо важные методы, просто дам вариант их реализации
  override def getParticleTexture: TextureAtlasSprite = null//Определяет текстуру частиц блока и еды
  override def isAmbientOcclusion: Boolean = false
  override def isGui3d: Boolean = true
  override def isBuiltInRenderer = false
  override def getItemCameraTransforms: ItemCameraTransforms = ItemCameraTransforms.DEFAULT
}

Scala:
object CustomOverrideList extends ItemOverrideList{

  override def handleItemState(originalModel: IBakedModel, stack: ItemStack, world: World, entity: EntityLivingBase): IBakedModel = {//Возвращаем финальную модель, которой будет доступен стак
  //Метод вызывается очень часто(кажется, с частотой кадров), поэтому позже мы добавим мемоизацию(это не про те мемы)
    new WandFinalisedModel(originalModel, stack)
  }
}

Ну вот мы и дошли до самой интересной части. Делаем саму модель!
Нас сразу встречают две проблемы, которые в forge не удосужились решить:
  • Готовые примитивы фигур отсутствуют
  • Изменять масштабы и положение модели в зависимости от контекста рендера можно только изменением матрицы преобразования.

Жезл состоит из трех параллелепипедов, значит, нам нужен вспомогательный класс, создающиц 6 baked quad — столько граней у параллелепипеда.
Будем называть наш параллелепипед кубом, так короче и терминология геометрии нас не сильно беспокоит.

Мы выделим абстракцию куба, чтобы удобнее работать с каждой частью модели.
Cube принимает координаты первой точки, размеры и текстуру.
Scala:
object Cube{
  def apply(x: Float, y: Float, z: Float, w: Float, h: Float, d: Float,textureAtlasSprite: TextureAtlasSprite,color:(Float,Float,Float)=(1,1,1)): Cube = //Можно еще задать цвет, я это юзал для дебага
    new Cube(x, y, z, w, h, d,0,0,0,1,1,1,textureAtlasSprite,color)
}
case class Cube(
                 x:Float,y:Float,z:Float
                ,w:Float,h:Float,d:Float
                ,cx:Float,cy:Float,cz:Float
                ,scaleX:Float,scaleY:Float,scaleZ:Float,
                 textureAtlasSprite: TextureAtlasSprite,color:(Float,Float,Float)) {
  def toQuads: List[BakedQuad] =
    List(//Создаем грани
      createQuad1((x, y, z), (x, y+h, z), (x+w, y+h, z), (x+w, y, z), textureAtlasSprite),
      createQuad1((x, y, z), (x+w, y, z), (x+w, y, z+d), (x, y, z+d), textureAtlasSprite),
      createQuad1((x, y+h, z),(x, y, z), (x, y, z+d), (x, y+h, z+d), textureAtlasSprite),
      createQuad1((x+w, y, z+d),(x+w, y+h, z+d), (x, y+h, z+d), (x, y, z+d), textureAtlasSprite),
      createQuad1((x+w, y+h, z+d), (x+w, y+h, z), (x, y+h, z), (x, y+h, z+d), textureAtlasSprite),
      createQuad1((x+w, y+h, z+d), (x+w, y, z+d), (x+w, y, z),(x+w, y+h, z), textureAtlasSprite)
    )
  //позволяет удобно задавать вершины в виде трех координат (x,y,z)
  implicit private def tuple2Vec[F:Numeric](t:(F ,F ,F )): Vec3d = {
    import Numeric.Implicits._
    new Vec3d(t._1.toDouble,t._2.toDouble,t._3.toDouble)
  }
  val format: VertexFormat = net.minecraft.client.renderer.vertex.DefaultVertexFormats.ITEM
  private def putVertex(builder: UnpackedBakedQuad.Builder, normal: Vec3d, x: Double, y: Double, z: Double, u: Float, v: Float, sprite: TextureAtlasSprite,color:(Float,Float,Float)): Unit = {
    import net.minecraft.client.renderer.vertex.VertexFormatElement.EnumUsage._
    //format предполагает порядок следования свойств вершины, но мы об этом не задумываемся, код ниже определеяет порядок автоматически
    for (
      e <-0 until format.getElementCount
    ) {
      format.getElement(e).getUsage match {
        case POSITION =>
          builder.put(e, x.toFloat, y.toFloat, z.toFloat, 1.0f)
  
        case COLOR =>
          builder.put(e, color._1,color._2,color._3, 1.0f)
  
        case UV =>
          if (format.getElement(e).getIndex == 0) {
            val u1 = sprite.getInterpolatedU(u)
            val v1 = sprite.getInterpolatedV(v)
            builder.put(e, u1, v1, 0f, 1f)
    
          }
        case NORMAL =>
          builder.put(e, normal.xCoord.toFloat, normal.yCoord.toFloat, normal.zCoord.toFloat, 0f)
  
        case _ =>
          builder.put(e)
  
      }
    }
  }

  //масштаб, сжатие-расширение
  def extendedVectorScale(v1: Vec3d) = new Vec3d(v1.xCoord*scaleX,v1.yCoord*scaleY,v1.zCoord*scaleZ)

  private def createQuad1(v1: Vec3d, v2: Vec3d, v3: Vec3d, v4: Vec3d, sprite: TextureAtlasSprite, color:(Float,Float,Float)=color) = {
    val center=(cx*scaleX,cy*scaleY,cz*scaleZ)//смещение
    createQuad(extendedVectorScale(v1).add(center),extendedVectorScale(v2).add(center),extendedVectorScale(v3).add(center),extendedVectorScale(v4).add(center),sprite,color)
  }
  private def createQuad(v1: Vec3d, v2: Vec3d, v3: Vec3d, v4: Vec3d, sprite: TextureAtlasSprite,color:(Float,Float,Float)) = {
    //этот вектор определяет с какой стороны грань будет видна
    val normal:Vec3d = v1.subtract(v2).crossProduct(v3.subtract(v2))
    val builder = new UnpackedBakedQuad.Builder(format)//Вроде, самый простой способ создавать baked quad
    builder.setTexture(sprite)
    putVertex(builder, normal, v1.xCoord, v1.yCoord, v1.zCoord, 0, 0, sprite,color)
    putVertex(builder, normal, v2.xCoord, v2.yCoord, v2.zCoord, 0, 16, sprite,color)
    putVertex(builder, normal, v3.xCoord, v3.yCoord, v3.zCoord, 16, 16, sprite,color)
    putVertex(builder, normal, v4.xCoord, v4.yCoord, v4.zCoord, 16, 0, sprite,color)
    builder.build
  }
  //изменение текущего куба
  def scale(sx:Float,sy:Float,sz:Float): Cube = copy(scaleX=scaleX*sx,scaleY=scaleY*sy,scaleZ=scaleZ*sz)
  def scale(s:Float): Cube = copy(scaleX=scaleX*s,scaleY=scaleY*s,scaleZ=scaleZ*s)
  def move(cx1:Float,cy1:Float,cz1:Float): Cube = copy(cx=cx+cx1,cy=cy+cy1,cz=cz+cz1)
}

Юзаем получившийся «кубик» в финальной модели
Scala:
class WandFinalisedModel(parentModel: IbakedModel, itemStack:ItemStack) extends IPerspectiveAwareModel {
  private val textureRod = Minecraft.getMinecraft.getTextureMapBlocks.getAtlasSprite(ItemRendererManager.textureRod.toString)
  private val textureCap = Minecraft.getMinecraft.getTextureMapBlocks.getAtlasSprite(ItemRendererManager.textureCap.toString)
  override def getQuads(@Nullable state: IBlockState, @Nullable side: EnumFacing, rand: Long): util.List[BakedQuad] = {
    if (side != null) parentModel.getQuads(state, side, rand)
    else {
      val combinedQuadsList = new util.ArrayList(parentModel.getQuads(state, side, rand))//На случай, если вы захотите юзать полигоны из json-модели
      val fl = 0.2f
      combinedQuadsList.addAll(
        (
          Cube(-1, -1, -1, 2, 2, 2,textureCap).scale(1.2f,1,1.2f).scale(fl).toQuads++//нижний наконечник
          Cube(-1, -1, -1, 2, 2, 2,textureCap).scale(1.2f,1,1.2f).scale(fl).move(0, 20, 0).toQuads++//верхний
          Cube(-1, -1, -1, 2, 18, 2,textureRod).scale(fl).move(0, 2, 0).toQuads//стержень
        ).asJava
      )
      combinedQuadsList
    }
  }

//Разбор нормальной реализации этого метода можно будет найти во второй части гайда(скорее всего не будет)
  override def handlePerspective(cameraTransformType: ItemCameraTransforms.TransformType): Pair[_ <: IBakedModel, Matrix4f] =
parentModel.handlePerspective(cameraTransformType)
    override def getOverrides = throw new UnsupportedOperationException("The finalised model does not have an override list")
}

Теперь мы видим модель жезла, но она имеет текстуру алмазного блока.
1528033802050.png

Давайте использовать текстуру из ресурсов! (/main/res/assets/<modid>/)
В качестве текстуры можно использовать TextureAtlasSprite.
Чтобы получить атлас для произвольного ResourceLocation можно воспользоваться событием TextureStitchEvent:
Scala:
@SubscribeEvent
def stitcherEventPre(event:TextureStitchEvent.Pre) {
  //Регаем все текстуры, добавленные во время preInit в лист
  ItemRendererManager.forRegister.foreach(event.getMap.registerSprite)
}
private val forRegister=new ListBuffer[ResourceLocation]
//Юзаем этот метод в preInit
def registerTexture(resourceLocation: ResourceLocation): Unit = forRegister+=resourceLocation

Достаем атлас так:
Minecraft.getMinecraft.getTextureMapBlocks.getAtlasSprite(<resourceLocation>.toString)

Все отлично, вот только наша модель выглядит странно - непропорционально и вертикально в инвентаре:
1528033885738.png


Мы исправим это в следующем уроке — Реализация метода IBakedModel#handlePerspective(когда допишу его это будет кликабельно)

Оптимизации
WandItemOverrideList#handleItemState
Scala:
private val memoization = new mutable.OpenHashMap[String,WandFinalisedModel]()

private def model(originalModel: IBakedModel, stack: ItemStack) = {
  val key=stack.getDisplayName//По идее, жезлы отличаются материалами, из которых сделаны(т.е. нбт), но для упрощения пусть они будут отличаться именем стака
  memoization.getOrElse(key,
    new WandFinalisedModel(originalModel,stack, key)//Второй аргумент вычисляется лениво, только если в мапе не найдется модели
  )
}

override def handleItemState(originalModel: IBakedModel, stack: ItemStack, world: World, entity: EntityLivingBase): IBakedModel =
  model(originalModel, stack)

WandFinalisedModel#getQuads
Эта штука тоже вызывается часто и от переменной rand не зависит, поэтому стоит добавить мемоизацию
Scala:
private var memoization = new mutable.OpenHashMap[IBlockState,util.List[BakedQuad]]()

private def quads(state: IBlockState): util.List[BakedQuad] = {
  memoization.getOrElse(state, {
    val combinedQuadsList = new util.ArrayList(parentModel.getQuads(state, null, 0))
    val fl = 0.2f

    combinedQuadsList.addAll(
      (
        Cube(-1, -1, -1, 2, 2, 2,textureCap).scale(1.2f,1,1.2f).scale(fl).toQuads++

        Cube(-1, -1, -1, 2, 2, 2,textureCap).scale(1.2f,1,1.2f).scale(fl).move(0, 20, 0).toQuads++

        Cube(-1, -1, -1, 2, 18, 2,textureRod).scale(fl).move(0, 2, 0).toQuads
      ).asJava
    )

    combinedQuadsList
  })
}
override def getQuads(@Nullable state: IBlockState, @Nullable side: EnumFacing, rand: Long): util.List[BakedQuad] = {
  if (side != null)
    parentModel.getQuads(state, side, rand)
  else
    quads(state)
}

Заключение

По материалам первой части вы уже можете создавать свою модель для предмета без возни с json. Можете попробовать создать свои вспомогательные классы вроде Cube.
На этом пока все, надеюсь, вам понравилось :D
  • 1528034154725.png
    1528034154725.png
    18.7 KB · Просмотры: 913
Автор
hohserg
Скачивания
9
Просмотры
2,582
Первый выпуск
Обновление
Оценка
4.25 звёзд 8 оценок

Другие ресурсы пользователя hohserg

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

Хорошо обо всем рассказал, но код на скале мешает восприятию :(
hohserg
hohserg
Спасибо, я рад что тебе понравилось) Учи скалу, это топ язык, по сравнению в джавой
Гайд сам по себе интересный, но из-за того что Scala неудобоваримый. Основные принципы уловил, но такой код для большинства ничего хорошего не принесёт. Не охота сидеть с мануалом и переводить на JAVA. Но в общем и целом. Полезненько. Гут
Scala, scala... Налетели коршуны. ИМХО, туториал НЕ для начинающих.
К тому же, говорю как носитель священной жабы, не зная скалы, могу без проблем, записать весь вышеозначенный код на жаве. Не говорю, что всё просто, но не надо делать из мухи слона.
hohserg
hohserg
Дело в том, что те, кто просят на java просто привыкли копипастить
Хорошо, но соглашусь с Максиком, лучше бы на жабе а не на скале(
Скала не всем понятна, про блоки не рассказал
Удобно! Полезно! Нужно! Но Scala :(
Великолепно! Но я бы на твоем месте все таки переписал весь код на Java. На скале пишут единицы. И человек, не шарящий в скале, попросту не сможет воспользоваться этим тутором. Прошу переделать.
hohserg
hohserg
Спасибо) Но на Java писать очень тошно
Довольно полезно!
hohserg
hohserg
Рад, что кому-то помогло)
Сверху