- Версия(и) Minecraft
- 1.10+
Всем привет.
Вы уже знаете, что модели для предметов можно создавать при помощи json, в виде ресурсов.
В этом гайде будет рассмотрен альтернативный вариант создания моделей - при помощи кода.
Зачем оно?
На то есть две причины(как минимум).
И этот список продолжается еще дальше. Не много ли для таких довольно однотипных блоков?
При создании моделей кодом вы конечно же сможете повторно использовать код.
Основная идея
Нам доступно событие ModelBakeEvent, при помощи которого мы можем установить произвольную модель нашему предмету.
Мы можем создать наследников IBakedModel и IPerspectiveAwareModel, которые будут нашими моделями. В них интересны методы getOverrides: ItemOverrideList, getQuads: util.List[BakedQuad], handlePerspective
Первый нужен для динамических частей модели, второй возвращает статические.
Полигоны модели, представлены экземплярами BakedQuad — четырехвершинными полигонами.
А третий может задать матрицей изменение положения и размеров нашей модели в зависимости от того, где она находится: в инвентаре, валяется в мире, или игрок держит в руке. Имеет смысл выносить эти преобразования в отдельный метод, т.к. сами полигоны не меняются, меняется только положение и размер модели в целом
Ближе к делу
Мы будем создавать модель жезла из таумкрафт 4.
Возможно, вы подумали, что было бы здорово сделать блоки веток как в Туманном мире, но тему блоков(как и наклонных полигонов) я оставлю для одного из следующих уроков.
Должно получить че-то вроде:
Json пустой модели:
Наша модель-обертка
Ну вот мы и дошли до самой интересной части. Делаем саму модель!
Нас сразу встречают две проблемы, которые в forge не удосужились решить:
Жезл состоит из трех параллелепипедов, значит, нам нужен вспомогательный класс, создающиц 6 baked quad — столько граней у параллелепипеда.
Будем называть наш параллелепипед кубом, так короче и терминология геометрии нас не сильно беспокоит.
Мы выделим абстракцию куба, чтобы удобнее работать с каждой частью модели.
Cube принимает координаты первой точки, размеры и текстуру.
Юзаем получившийся «кубик» в финальной модели
Теперь мы видим модель жезла, но она имеет текстуру алмазного блока.
Давайте использовать текстуру из ресурсов! (/main/res/assets/<modid>/)
В качестве текстуры можно использовать TextureAtlasSprite.
Чтобы получить атлас для произвольного ResourceLocation можно воспользоваться событием TextureStitchEvent:
Достаем атлас так:
Все отлично, вот только наша модель выглядит странно - непропорционально и вертикально в инвентаре:
Мы исправим это в следующем уроке — Реализация метода IBakedModel#handlePerspective(когда допишу его это будет кликабельно)
Оптимизации
WandItemOverrideList#handleItemState
WandFinalisedModel#getQuads
Эта штука тоже вызывается часто и от переменной rand не зависит, поэтому стоит добавить мемоизацию
Заключение
По материалам первой части вы уже можете создавать свою модель для предмета без возни с json. Можете попробовать создать свои вспомогательные классы вроде Cube.
На этом пока все, надеюсь, вам понравилось
Вы уже знаете, что модели для предметов можно создавать при помощи json, в виде ресурсов.
В этом гайде будет рассмотрен альтернативный вариант создания моделей - при помощи кода.
Зачем оно?
На то есть две причины(как минимум).
- Во первых, для того, чтобы создавать динамические модели. Json не позволяет делать движущиеся и зависящие от нбт полигоны.
- Во вторых, json-ном нельзя сделать косых полигонов. А созданные кодом могут располагаться как вам угодно, под любыми углами.
- В третьих, давайте посмотрим что вас ждет, если вы будете обмазываться json-моделями:
И этот список продолжается еще дальше. Не много ли для таких довольно однотипных блоков?
При создании моделей кодом вы конечно же сможете повторно использовать код.
Основная идея
Нам доступно событие ModelBakeEvent, при помощи которого мы можем установить произвольную модель нашему предмету.
Мы можем создать наследников IBakedModel и IPerspectiveAwareModel, которые будут нашими моделями. В них интересны методы getOverrides: ItemOverrideList, getQuads: util.List[BakedQuad], handlePerspective
Первый нужен для динамических частей модели, второй возвращает статические.
Полигоны модели, представлены экземплярами BakedQuad — четырехвершинными полигонами.
А третий может задать матрицей изменение положения и размеров нашей модели в зависимости от того, где она находится: в инвентаре, валяется в мире, или игрок держит в руке. Имеет смысл выносить эти преобразования в отдельный метод, т.к. сами полигоны не меняются, меняется только положение и размер модели в целом
Ближе к делу
Мы будем создавать модель жезла из таумкрафт 4.
Возможно, вы подумали, что было бы здорово сделать блоки веток как в Туманном мире, но тему блоков(как и наклонных полигонов) я оставлю для одного из следующих уроков.
Должно получить че-то вроде:
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")
}
Теперь мы видим модель жезла, но она имеет текстуру алмазного блока.
Давайте использовать текстуру из ресурсов! (/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)
Все отлично, вот только наша модель выглядит странно - непропорционально и вертикально в инвентаре:
Мы исправим это в следующем уроке — Реализация метода 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.
На этом пока все, надеюсь, вам понравилось