Skip to content

Commit

Permalink
ModelBuilder UV scaling
Browse files Browse the repository at this point in the history
  • Loading branch information
NichtStudioCode committed May 12, 2024
1 parent 83cc558 commit b724971
Show file tree
Hide file tree
Showing 24 changed files with 1,219 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -161,27 +161,35 @@ class ModelBuilder(private val base: Model) {

/**
* Scales the model by [scale] to the given [pivot] point.
*
* With [scaleUV], the UV coordinates are scaled accordingly.
*/
fun scale(pivot: Vector3dc, scale: Vector3dc): ModelBuilder =
action(ScaleTransform(pivot, scale))
fun scale(pivot: Vector3dc, scale: Vector3dc, scaleUV: Boolean = false): ModelBuilder =
action(ScaleTransform(pivot, scale, scaleUV))

/**
* Scales the model by [scale].
*
* With [scaleUV], the UV coordinates are scaled accordingly.
*/
fun scale(scale: Vector3dc): ModelBuilder =
scale(CENTER, scale)
fun scale(scale: Vector3dc, scaleUV: Boolean = false): ModelBuilder =
scale(CENTER, scale, scaleUV)

/**
* Scales the model by the given factor [scale] for all axes to the given [pivot] point.
*
* With [scaleUV], the UV coordinates are scaled accordingly.
*/
fun scale(pivot: Vector3dc, scale: Double): ModelBuilder =
scale(pivot, Vector3d(scale, scale, scale))
fun scale(pivot: Vector3dc, scale: Double, scaleUV: Boolean = false): ModelBuilder =
scale(pivot, Vector3d(scale, scale, scale), scaleUV)

/**
* Scales the model by the given factor [scale] for all axes to the pivot point (8, 8, 8).
*
* With [scaleUV], the UV coordinates are scaled accordingly.
*/
fun scale(scale: Double): ModelBuilder =
scale(Vector3d(scale, scale, scale))
fun scale(scale: Double, scaleUV: Boolean = false): ModelBuilder =
scale(Vector3d(scale, scale, scale), scaleUV)

/**
* Combines this model with the given [model] by flattening the elements of the other [model] and
Expand Down Expand Up @@ -263,7 +271,7 @@ class ModelBuilder(private val base: Model) {
if (bounds.minZ < MIN_ELEMENT_FROM)
scale = min(scale, (pivot.z - MIN_ELEMENT_FROM) / (pivot.z - bounds.minZ))

resultModel = ScaleTransform(pivot, Vector3d(scale, scale, scale), true).apply(resultModel)
resultModel = ScaleTransform(pivot, Vector3d(scale, scale, scale), keepDisplaySize = true).apply(resultModel)
}

return ScaledModel(resultModel, 1 / scale)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ internal data class RotationTransform(
) : NonContextualModelBuildAction, Transform {

override fun apply(matrix: Matrix4d) {
if (uvLock)
throw UnsupportedOperationException("UV lock is not supported in matrix transformations")
if (rescale) // TODO: this can be implemented
throw UnsupportedOperationException("Rescale is not supported in matrix transformations")

matrix.translate(-(8 - pivot.x()) / 16, -(8 - pivot.y()) / 16, -(8 - pivot.z()) / 16)
when (axis) {
Axis.X -> matrix.rotateX(Math.toRadians(rot))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
package xyz.xenondevs.nova.data.resources.builder.model.transform

import org.joml.Matrix4d
import org.joml.Vector2d
import org.joml.Vector2dc
import org.joml.Vector3d
import org.joml.Vector3dc
import org.joml.Vector4d
import org.joml.Vector4dc
import xyz.xenondevs.nova.data.resources.builder.model.Model

/**
* A transformation that scales a model by [scale] around [pivot].
*
* @param pivot the pivot point around which the model is scaled
* @param scale the scale factor
* @param scaleUV whether to scale the UV coordinates of the model,
* trying to preserve the pixel size
* @param keepDisplaySize whether the display size properties should be
* inversely scaled in order to keep the actual displayed size constant
*/
internal data class ScaleTransform(
val pivot: Vector3dc,
val scale: Vector3dc,
val scaleUV: Boolean = false,
val keepDisplaySize: Boolean = false
) : NonContextualModelBuildAction, Transform {

Expand All @@ -20,7 +32,12 @@ internal data class ScaleTransform(
element.copy(
from = scaled(element.from), to = scaled(element.to),
rotation = element.rotation?.let { it.copy(origin = scaled(it.origin)) },
faces = element.faces.mapValues { (direction, face) -> face.copy(uv = face.uv ?: element.generateUV(direction)) }
faces = element.faces.mapValues { (direction, face) ->
var uv = face.uv ?: element.generateUV(direction)
if (scaleUV)
uv = scaledUV(direction, element.from, element.to, uv, face.rotation)
face.copy(uv = uv)
}
)
}

Expand All @@ -36,7 +53,96 @@ internal data class ScaleTransform(
private fun scaled(v: Vector3dc): Vector3dc =
Vector3d(v).sub(pivot).mul(scale).add(pivot)

/**
* Adjusts the [uv] for [face] with [rotation] of an element with positions [from] and [to].
*/
private fun scaledUV(face: Model.Direction, from: Vector3dc, to: Vector3dc, uv: Vector4dc, rotation: Int): Vector4dc {
val (uv0, uAxis, vAxis) = getUVAxes(face, rotation)

val elementSize = Vector3d(to).sub(from)
val relPivot = Vector3d(pivot)
.div(elementSize)
.sub(uv0)
val uvSize = Vector2d(uv.z(), uv.w()).sub(uv.x(), uv.y())
val uvScale = Vector2d(uAxis.dot(scale), vAxis.dot(scale)).absolute()
val uvPivot = Vector2d(uAxis.dot(relPivot), vAxis.dot(relPivot))
.mul(uvSize)
.add(Vector2d(uv.x(), uv.y()))

return scaledUV(uv, uvPivot, uvScale)
}

/**
* Adjusts the [uv] based on the [uvPivot] and [uvScale].
*/
private fun scaledUV(uv: Vector4dc, uvPivot: Vector2dc, uvScale: Vector2dc): Vector4dc {
val from = Vector2d(uv.x(), uv.y())
val to = Vector2d(uv.z(), uv.w())

from.sub(uvPivot).mul(uvScale).add(uvPivot)
to.sub(uvPivot).mul(uvScale).add(uvPivot)

return Vector4d(from.x(), from.y(), to.x(), to.y())
}

/**
* Gets a tuple the uv axes and normalized UV origin for the given face and rotation. (uv0, uAxis, vAxis)
*/
private fun getUVAxes(face: Model.Direction, rotation: Int): Triple<Vector3d, Vector3d, Vector3d> {
val uv0 = Vector3d(0.0, 1.0, 1.0)
val u = Vector3d(1.0, 0.0, 0.0)
val v = Vector3d(0.0, -1.0, 0.0)

fun rotate(axis: Model.Axis, angleDeg: Double) {
val angleRad = Math.toRadians(angleDeg)
uv0.sub(0.5, 0.5, 0.5).rotate(axis, angleRad).add(0.5, 0.5, 0.5)
v.rotate(axis, angleRad)
u.rotate(axis, angleRad)
}

when (face) {
Model.Direction.SOUTH -> {
rotate(Model.Axis.Z, -rotation.toDouble())
}
Model.Direction.EAST ->{
rotate(Model.Axis.Y, 90.0)
rotate(Model.Axis.X, -rotation.toDouble())
}
Model.Direction.NORTH ->{
rotate(Model.Axis.Y, 180.0)
rotate(Model.Axis.Z, rotation.toDouble())
}
Model.Direction.WEST -> {
rotate(Model.Axis.Y, 270.0)
rotate(Model.Axis.X, rotation.toDouble())
}
Model.Direction.UP -> {
rotate(Model.Axis.X, -90.0)
rotate(Model.Axis.Y, -rotation.toDouble())
}
Model.Direction.DOWN -> {
rotate(Model.Axis.X, 90.0)
rotate(Model.Axis.Y, rotation.toDouble())
}
}

return Triple(uv0, u, v)
}

/**
* Rotates [this][Vector3d] by [angleRad] radians around [axis].
*/
private fun Vector3d.rotate(axis: Model.Axis, angleRad: Double): Vector3d =
when (axis) {
Model.Axis.X -> rotateX(angleRad)
Model.Axis.Y -> rotateY(angleRad)
Model.Axis.Z -> rotateZ(angleRad)
}

override fun apply(matrix: Matrix4d) {
if (scaleUV)
throw UnsupportedOperationException("Cannot apply UV adjustments to a matrix")

matrix.translate(-(8 - pivot.x()) / 16, -(8 - pivot.y()) / 16, -(8 - pivot.z()) / 16)
matrix.scale(scale)
matrix.translate((8 - pivot.x()) / 16, (8 - pivot.y()) / 16, (8 - pivot.z()) / 16)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@ class ModelBuilderTest {
assertEquals(deserializeModel("full_cube/scaled/half_uncentered"), halfUncentered)
}

@Test
fun testScaleUV() {
val rots = listOf(0, 90, 180, 270)
for (rot in rots) {
val model = deserializeModel("arrow_cube/$rot/model")

val m1 = ModelBuilder(model)
.scale(Vector3d(0.0, 0.0, 0.0), Vector3d(0.5, 1.0, 0.25), true)
.buildScaled(null)
assertEquals(deserializeModel("arrow_cube/$rot/scaled/pillar_0_0"), m1)

val m2 = ModelBuilder(model)
.scale(Vector3d(16.0, 0.0, 16.0), Vector3d(0.5, 1.0, 0.25), true)
.buildScaled(null)
assertEquals(deserializeModel("arrow_cube/$rot/scaled/pillar_16_16"), m2)

val m3 = ModelBuilder(model)
.scale(Vector3d(8.0, 8.0, 8.0), Vector3d(0.5, 0.5, 0.5), true)
.buildScaled(null)
assertEquals(deserializeModel("arrow_cube/$rot/scaled/cube_centered"), m3)
}
}

@Test
fun testTranslate() {
val model = deserializeModel("full_cube/model")
Expand Down Expand Up @@ -167,7 +190,7 @@ class ModelBuilderTest {
}

private fun deserializeModel(name: String): Model =
javaClass.getResourceAsStream("/models/$name.json")?.use { GSON.fromJson<Model>(it.reader())!! }
javaClass.getResourceAsStream("/models/$name.json")?.use { GSON.fromJson<Model>(it.reader())!! }
?: throw IllegalArgumentException("Model $name not found")

private fun serializeModel(model: Model): String =
Expand Down
19 changes: 19 additions & 0 deletions nova/src/test/resources/models/arrow_cube/0/model.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"textures": {
"1": "block/magenta_glazed_terracotta"
},
"elements": [
{
"from": [0, 0, 0],
"to": [16, 16, 16],
"faces": {
"north": {"uv": [0, 0, 16, 16], "texture": "#1"},
"east": {"uv": [0, 0, 16, 16], "texture": "#1"},
"south": {"uv": [0, 0, 16, 16], "texture": "#1"},
"west": {"uv": [0, 0, 16, 16], "texture": "#1"},
"up": {"uv": [0, 0, 16, 16], "texture": "#1"},
"down": {"uv": [0, 0, 16, 16], "texture": "#1"}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"textures": {
"1": "block/magenta_glazed_terracotta"
},
"elements": [
{
"from": [
4.0,
4.0,
4.0
],
"to": [
12.0,
12.0,
12.0
],
"faces": {
"north": {
"uv": [
3.999999999999999,
4.0,
12.0,
12.0
],
"texture": "#1"
},
"east": {
"uv": [
4.0,
4.0,
12.0,
12.0
],
"texture": "#1"
},
"south": {
"uv": [
4.0,
4.0,
12.0,
12.0
],
"texture": "#1"
},
"west": {
"uv": [
4.0,
4.0,
12.0,
12.0
],
"texture": "#1"
},
"up": {
"uv": [
4.0,
4.0,
12.0,
12.0
],
"texture": "#1"
},
"down": {
"uv": [
4.0,
4.0,
12.0,
12.0
],
"texture": "#1"
}
}
}
]
}
Loading

0 comments on commit b724971

Please sign in to comment.