first Kotlin version

This commit is contained in:
octarine-noise
2015-12-28 11:49:46 +01:00
parent 554e06176b
commit f44043bb0b
143 changed files with 5469 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
package mods.octarinecore.client
import cpw.mods.fml.client.registry.ClientRegistry
import cpw.mods.fml.common.FMLCommonHandler
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import cpw.mods.fml.common.gameevent.InputEvent
import net.minecraft.client.settings.KeyBinding
class KeyHandler(val modId: String, val defaultKey: Int, val lang: String, val action: (InputEvent.KeyInputEvent)->Unit) {
val keyBinding = KeyBinding(lang, defaultKey, modId)
init {
ClientRegistry.registerKeyBinding(keyBinding)
FMLCommonHandler.instance().bus().register(this)
}
@SubscribeEvent
fun handleKeyPress(event: InputEvent.KeyInputEvent) {
if (keyBinding.isPressed) action(event)
}
}

View File

@@ -0,0 +1,58 @@
package mods.octarinecore.client.gui
import cpw.mods.fml.client.config.*
import net.minecraft.client.gui.GuiScreen
import net.minecraft.client.resources.I18n
import net.minecraft.util.EnumChatFormatting.GOLD
import net.minecraft.util.EnumChatFormatting.YELLOW
/**
* Base class for a config GUI element.
* The GUI representation is a list of toggleable objects.
* The config representation is an integer list of the selected objects' IDs.
*/
abstract class IdListConfigEntry<T>(
owningScreen: GuiConfig,
owningEntryList: GuiConfigEntries,
configElement: IConfigElement<*>
) : GuiConfigEntries.CategoryEntry(owningScreen, owningEntryList, configElement) {
/** Create the child GUI elements. */
fun createChildren() = baseSet.map {
ItemWrapperElement(it, it.itemId in configElement.list, it.itemId in configElement.defaults)
}
init { stripTooltipDefaultText(toolTip as MutableList<String>) }
override fun buildChildScreen(): GuiScreen {
return GuiConfig(this.owningScreen, createChildren(), this.owningScreen.modID,
owningScreen.allRequireWorldRestart || this.configElement.requiresWorldRestart(),
owningScreen.allRequireMcRestart || this.configElement.requiresMcRestart(), this.owningScreen.title,
((if (this.owningScreen.titleLine2 == null) "" else this.owningScreen.titleLine2) + " > " + this.name))
}
override fun saveConfigElement(): Boolean {
val requiresRestart = (childScreen as GuiConfig).entryList.saveConfigElements()
val children = (childScreen as GuiConfig).configElements as List<ItemWrapperElement>
val ids = children.filter { it.booleanValue == true }.map { it.item.itemId }
configElement.set(ids.sorted().toTypedArray())
return requiresRestart
}
abstract val baseSet: List<T>
abstract val T.itemId: Int
abstract val T.itemName: String
/** Child config GUI element of a single toggleable object. */
inner class ItemWrapperElement(val item: T, value: Boolean, val default: Boolean) :
DummyConfigElement<Boolean>(item.itemName, default, ConfigGuiType.BOOLEAN, item.itemName) {
init { set(value) }
override fun getComment() = I18n.format("${configElement.languageKey}.tooltip.element", "${GOLD}${item.itemName}${YELLOW}")
override fun set(value: Boolean) { this.value = value }
fun setDefault(value: Boolean) { this.defaultValue = value }
val booleanValue: Boolean get() = value as Boolean
}
}

View File

@@ -0,0 +1,25 @@
package mods.octarinecore.client.gui
import cpw.mods.fml.client.config.GuiConfig
import cpw.mods.fml.client.config.GuiConfigEntries
import cpw.mods.fml.client.config.IConfigElement
import net.minecraft.client.resources.I18n
import net.minecraft.util.EnumChatFormatting.*
class NonVerboseArrayEntry(
owningScreen: GuiConfig,
owningEntryList: GuiConfigEntries,
configElement: IConfigElement<*>
) : GuiConfigEntries.ArrayEntry(owningScreen, owningEntryList, configElement) {
init {
stripTooltipDefaultText(toolTip as MutableList<String>)
val shortDefaults = I18n.format("${configElement.languageKey}.arrayEntry", configElement.defaults.size)
toolTip.addAll(mc.fontRenderer.listFormattedStringToWidth("$AQUA${I18n.format("fml.configgui.tooltip.default", shortDefaults)}", 300))
}
override fun updateValueButtonText() {
btnValue.displayString = I18n.format("${configElement.languageKey}.arrayEntry", currentValues.size)
}
}

View File

@@ -0,0 +1,13 @@
@file:JvmName("Utils")
package mods.octarinecore.client.gui
import net.minecraft.util.EnumChatFormatting
fun stripTooltipDefaultText(tooltip: MutableList<String>) {
var defaultRows = false
val iter = tooltip.iterator()
while (iter.hasNext()) {
if (iter.next().startsWith(EnumChatFormatting.AQUA.toString())) defaultRows = true
if (defaultRows) iter.remove()
}
}

View File

@@ -0,0 +1,160 @@
@file:JvmName("RendererHolder")
package mods.octarinecore.client.render
import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler
import cpw.mods.fml.client.registry.RenderingRegistry
import mods.octarinecore.ThreadLocalDelegate
import mods.octarinecore.client.resource.ResourceHandler
import net.minecraft.block.Block
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.RenderBlocks
import net.minecraft.util.IIcon
import net.minecraft.util.MathHelper
import net.minecraft.world.IBlockAccess
import net.minecraftforge.common.util.ForgeDirection
/**
* [ThreadLocal] instance of [ExtendedRenderBlocks] used instead of the vanilla [RenderBlocks] to get the
* AO values and textures used in rendering without duplicating vanilla code.
*/
val renderBlocks by ThreadLocalDelegate { ExtendedRenderBlocks() }
/**
* [ThreadLocal] instance of [BlockContext] representing the block being rendered.
*/
val blockContext by ThreadLocalDelegate { BlockContext() }
/**
* [ThreadLocal] instance of [ModelRenderer].
*/
val modelRenderer by ThreadLocalDelegate { ModelRenderer() }
abstract class AbstractBlockRenderingHandler(modId: String) : ResourceHandler(modId), ISimpleBlockRenderingHandler {
// ============================
// Self-registration
// ============================
val id = RenderingRegistry.getNextAvailableRenderId()
init {
RenderingRegistry.registerBlockHandler(this);
}
// ============================
// Custom rendering
// ============================
abstract fun isEligible(ctx: BlockContext): Boolean
abstract fun render(ctx: BlockContext, parent: RenderBlocks): Boolean
// ============================
// Interface implementation
// ============================
override fun renderWorldBlock(world: IBlockAccess?, x: Int, y: Int, z: Int, block: Block?, modelId: Int, parentRenderer: RenderBlocks?): Boolean {
renderBlocks.blockAccess = world
return render(blockContext, parentRenderer!!)
}
override fun renderInventoryBlock(block: Block?, metadata: Int, modelId: Int, renderer: RenderBlocks?) {}
override fun shouldRender3DInInventory(modelId: Int) = true
override fun getRenderId(): Int = id
// ============================
// Vanilla rendering wrapper
// ============================
/**
* Render the block in the current [BlockContext], and capture shading and texture data.
*
* @param[parentRenderer] parent renderer passed in by rendering pipeline, used only for block breaking overlay
* @param[targetPass] which render pass to save shading and texture data from
* @param[block] lambda to use to render the block if it does not have a custom renderer
* @param[face] lambda to determine which faces of the block to render
*/
fun renderWorldBlockBase(
parentRenderer: RenderBlocks = renderBlocks,
targetPass: Int = 1,
block: () -> Unit = { blockContext.let { ctx -> renderBlocks.renderStandardBlock(ctx.block, ctx.x, ctx.y, ctx.z) } },
face: (ShadingCapture, ForgeDirection, Int, IIcon?) -> Boolean
): Boolean {
val ctx = blockContext
val renderBlocks = renderBlocks
// use original renderer for block breaking overlay
if (parentRenderer.hasOverrideBlockTexture()) {
parentRenderer.setRenderBoundsFromBlock(ctx.block);
parentRenderer.renderStandardBlock(ctx.block, ctx.x, ctx.y, ctx.z);
return true;
}
// render block
renderBlocks.capture.reset(targetPass)
renderBlocks.capture.renderCallback = face
renderBlocks.setRenderBoundsFromBlock(ctx.block);
val handler = renderingHandlers[ctx.block.renderType];
if (handler != null && ctx.block.renderType != 0) {
handler.renderWorldBlock(ctx.world, ctx.x, ctx.y, ctx.z, ctx.block, ctx.block.renderType, renderBlocks);
} else {
block()
}
return false;
}
}
/**
* Represents the block being rendered. Has properties and methods to query the neighborhood of the block in
* block-relative coordinates.
*/
class BlockContext() {
var world: IBlockAccess? = null
var x: Int = 0
var y: Int = 0
var z: Int = 0
fun set(world: IBlockAccess, x: Int, y: Int, z: Int) { this.world = world; this.x = x; this.y = y; this.z = z; }
/** Get the [Block] at the given offset. */
val block: Block get() = world!!.getBlock(x, y, z)
fun block(offset: Int3) = world!!.getBlock(x + offset.x, y + offset.y, z + offset.z)
/** Get the metadata at the given offset. */
val meta: Int get() = world!!.getBlockMetadata(x, y, z)
fun meta(offset: Int3) = world!!.getBlockMetadata(x + offset.x, y + offset.y, z + offset.z)
/** Get the block color multiplier at the given offset. */
val blockColor: Int get() = block.colorMultiplier(world, x, y, z)
fun blockColor(offset: Int3) = block(offset).colorMultiplier(world, x + offset.x, y + offset.y, z + offset.z)
/** Get the block brightness at the given offset. */
val blockBrightness: Int get() = block.getMixedBrightnessForBlock(world, x, y, z)
fun blockBrightness(offset: Int3) = block(offset).getMixedBrightnessForBlock(world, x + offset.x, y + offset.y, z + offset.z)
/** Get the biome ID at the block position. */
val biomeId: Int get() = world!!.getBiomeGenForCoords(x, z).biomeID
/** Get the texture on a given face of the block being rendered. */
fun icon(face: ForgeDirection) = block(Int3.zero).getIcon(face.ordinal, meta(Int3.zero))
/** Get the texture on a given face of the block at the given offset. */
fun icon(offset: Int3, face: ForgeDirection) = block(offset).getIcon(face.ordinal, meta(offset))
/** Get the centerpoint of the block being rendered. */
val blockCenter: Double3 get() = Double3(x + 0.5, y + 0.5, z + 0.5)
/** Is the block surrounded by other blocks that satisfy the predicate on all sides? */
fun isSurroundedBy(predicate: (Block)->Boolean) = forgeDirOffsets.all { predicate(block(it)) }
/** Get a semi-random value based on the block coordinate and the given seed. */
fun random(seed: Int): Int {
var value = (x * x + y * y + z * z + x * y + y * z + z * x + (seed * seed)) and 63
value = (3 * x * value + 5 * y * value + 7 * z * value + (11 * seed)) and 63
return value
}
/** Get an array of semi-random values based on the block coordinate. */
fun semiRandomArray(num: Int): Array<Int> = Array(num) { random(it) }
/** Get the distance of the block from the camera (player). */
val cameraDistance: Int get() {
val camera = Minecraft.getMinecraft().renderViewEntity ?: return 0
return Math.abs(x - MathHelper.floor_double(camera.posX)) +
Math.abs(y - MathHelper.floor_double(camera.posY)) +
Math.abs(z - MathHelper.floor_double(camera.posZ))
}
}

View File

@@ -0,0 +1,98 @@
package mods.octarinecore.client.render
import mods.octarinecore.PI2
import net.minecraft.client.Minecraft
import net.minecraft.client.particle.EntityFX
import net.minecraft.client.renderer.Tessellator
import net.minecraft.util.IIcon
import net.minecraft.world.World
abstract class AbstractEntityFX(world: World, x: Double, y: Double, z: Double) : EntityFX(world, x, y, z) {
companion object {
@JvmStatic val sin = Array(64) { idx -> Math.sin(PI2 / 64.0 * idx) }
@JvmStatic val cos = Array(64) { idx -> Math.cos(PI2 / 64.0 * idx) }
}
val billboardRot = Pair(Double3.zero, Double3.zero)
val currentPos = Double3.zero
val prevPos = Double3.zero
val velocity = Double3.zero
override fun onUpdate() {
super.onUpdate()
currentPos.setTo(posX, posY, posZ)
prevPos.setTo(prevPosX, prevPosY, prevPosZ)
velocity.setTo(motionX, motionY, motionZ)
update()
posX = currentPos.x; posY = currentPos.y; posZ = currentPos.z;
motionX = velocity.x; motionY = velocity.y; motionZ = velocity.z;
}
/** Render the particle. */
abstract fun render(tessellator: Tessellator, partialTickTime: Float)
/** Update particle on world tick. */
abstract fun update()
/** True if the particle is renderable. */
abstract val isValid: Boolean
/** Add the particle to the effect renderer if it is valid. */
fun addIfValid() { if (isValid) Minecraft.getMinecraft().effectRenderer.addEffect(this) }
override fun renderParticle(tessellator: Tessellator, partialTickTime: Float, rotX: Float, rotZ: Float, rotYZ: Float, rotXY: Float, rotXZ: Float) {
billboardRot.first.setTo(rotX + rotXY, rotZ, rotYZ + rotXZ)
billboardRot.second.setTo(rotX - rotXY, -rotZ, rotYZ - rotXZ)
render(tessellator, partialTickTime)
}
/**
* Render a particle quad.
*
* @param[tessellator] the [Tessellator] instance to use
* @param[partialTickTime] partial tick time
* @param[currentPos] render position
* @param[prevPos] previous tick position for interpolation
* @param[size] particle size
* @param[rotation] viewpoint-dependent particle rotation (64 steps)
* @param[icon] particle texture
* @param[isMirrored] mirror particle texture along V-axis
* @param[alpha] aplha blending
*/
fun renderParticleQuad(tessellator: Tessellator,
partialTickTime: Float,
currentPos: Double3 = this.currentPos,
prevPos: Double3 = this.prevPos,
size: Double = particleScale.toDouble(),
rotation: Int = 0,
icon: IIcon = particleIcon,
isMirrored: Boolean = false,
alpha: Float = this.particleAlpha) {
val minU = (if (isMirrored) icon.minU else icon.maxU).toDouble()
val maxU = (if (isMirrored) icon.maxU else icon.minU).toDouble()
val minV = icon.minV.toDouble()
val maxV = icon.maxV.toDouble()
val center = currentPos.copy().sub(prevPos).mul(partialTickTime.toDouble()).add(prevPos).sub(interpPosX, interpPosY, interpPosZ)
val v1 = if (rotation == 0) billboardRot.first * size else
Double3.weight(billboardRot.first, cos[rotation and 63] * size, billboardRot.second, sin[rotation and 63] * size)
val v2 = if (rotation == 0) billboardRot.second * size else
Double3.weight(billboardRot.first, -sin[rotation and 63] * size, billboardRot.second, cos[rotation and 63] * size)
tessellator.setColorRGBA_F(this.particleRed, this.particleGreen, this.particleBlue, alpha)
tessellator.addVertexWithUV(center.x - v1.x, center.y - v1.y, center.z - v1.z, maxU, maxV)
tessellator.addVertexWithUV(center.x - v2.x, center.y - v2.y, center.z - v2.z, maxU, minV)
tessellator.addVertexWithUV(center.x + v1.x, center.y + v1.y, center.z + v1.z, minU, minV)
tessellator.addVertexWithUV(center.x + v2.x, center.y + v2.y, center.z + v2.z, minU, maxV)
}
override fun getFXLayer() = 1
fun setColor(color: Int) {
particleBlue = (color and 255) / 256.0f
particleGreen = ((color shr 8) and 255) / 256.0f
particleRed = ((color shr 16) and 255) / 256.0f
}
}

View File

@@ -0,0 +1,214 @@
package mods.octarinecore.client.render
import mods.octarinecore.client.render.Axis.*
import mods.octarinecore.client.render.Dir.N
import mods.octarinecore.client.render.Dir.P
import mods.octarinecore.cross
import net.minecraftforge.common.util.ForgeDirection
import net.minecraftforge.common.util.ForgeDirection.*
// ================================
// Axes and directions
// ================================
enum class Axis { X, Y, Z }
enum class Dir { P, N }
val axes = listOf(X, Y, Z)
val axisDirs = listOf(P, N)
val forgeDirs = ForgeDirection.VALID_DIRECTIONS
val forgeDirOffsets = forgeDirs.map { Int3(it) }
val ForgeDirection.axis: Axis get() = when(this) {EAST, WEST -> X; UP, DOWN -> Y; else -> Z }
val ForgeDirection.dir: Dir get() = when(this) {UP, SOUTH, EAST -> P; else -> N }
val Pair<Axis, Dir>.face: ForgeDirection get() = when(this) {
X to P -> EAST; X to N -> WEST; Y to P -> UP; Y to N -> DOWN; Z to P -> SOUTH; Z to N -> NORTH; else -> UNKNOWN
}
val ForgeDirection.perpendiculars: List<ForgeDirection> get() =
axes.filter { it != this.axis }.cross(axisDirs).map { it.face }
val ForgeDirection.offset: Int3 get() = forgeDirOffsets[ordinal]
// ================================
// Vectors
// ================================
operator fun ForgeDirection.times(scale: Double) =
Double3(offsetX.toDouble() * scale, offsetY.toDouble() * scale, offsetZ.toDouble() * scale)
val ForgeDirection.vec: Double3 get() = Double3(offsetX.toDouble(), offsetY.toDouble(), offsetZ.toDouble())
/** 3D vector of [Double]s. Offers both mutable operations, and immutable operations in operator notation. */
data class Double3(var x: Double, var y: Double, var z: Double) {
constructor(x: Float, y: Float, z: Float) : this(x.toDouble(), y.toDouble(), z.toDouble())
constructor(dir: ForgeDirection) : this(dir.offsetX.toDouble(), dir.offsetY.toDouble(), dir.offsetZ.toDouble())
companion object {
val zero: Double3 get() = Double3(0.0, 0.0, 0.0)
fun weight(v1: Double3, weight1: Double, v2: Double3, weight2: Double) =
Double3(v1.x * weight1 + v2.x * weight2, v1.y * weight1 + v2.y * weight2, v1.z * weight1 + v2.z * weight2)
}
// immutable operations
operator fun plus(other: Double3) = Double3(x + other.x, y + other.y, z + other.z)
operator fun unaryMinus() = Double3(-x, -y, -z)
operator fun minus(other: Double3) = Double3(x - other.x, y - other.y, z - other.z)
operator fun times(scale: Double) = Double3(x * scale, y * scale, z * scale)
operator fun times(other: Double3) = Double3(x * other.x, y * other.y, z * other.z)
/** Rotate this vector, and return coordinates in the unrotated frame */
fun rotate(rot: Rotation) = Double3(
rot.rotatedComponent(EAST, x, y, z),
rot.rotatedComponent(UP, x, y, z),
rot.rotatedComponent(SOUTH, x, y, z)
)
// mutable operations
fun setTo(other: Double3): Double3 { x = other.x; y = other.y; z = other.z; return this }
fun setTo(x: Double, y: Double, z: Double): Double3 { this.x = x; this.y = y; this.z = z; return this }
fun setTo(x: Float, y: Float, z: Float) = setTo(x.toDouble(), y.toDouble(), z.toDouble())
fun add(other: Double3): Double3 { x += other.x; y += other.y; z += other.z; return this }
fun add(x: Double, y: Double, z: Double): Double3 { this.x += x; this.y += y; this.z += z; return this }
fun sub(other: Double3): Double3 { x -= other.x; y -= other.y; z -= other.z; return this }
fun sub(x: Double, y: Double, z: Double): Double3 { this.x -= x; this.y -= y; this.z -= z; return this }
fun invert(): Double3 { x = -x; y = -y; z = -z; return this }
fun mul(scale: Double): Double3 { x *= scale; y *= scale; z *= scale; return this }
fun mul(other: Double3): Double3 { x *= other.x; y *= other.y; z *= other.z; return this }
fun rotateMut(rot: Rotation): Double3 {
val rotX = rot.rotatedComponent(EAST, x, y, z)
val rotY = rot.rotatedComponent(UP, x, y, z)
val rotZ = rot.rotatedComponent(SOUTH, x, y, z)
return setTo(rotX, rotY, rotZ)
}
// misc operations
infix fun dot(other: Double3) = x * other.x + y * other.y + z * other.z
infix fun cross(o: Double3) = Double3(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x)
val length: Double get() = Math.sqrt(x * x + y * y + z * z)
val normalize: Double3 get() = (1.0 / length).let { Double3(x * it, y * it, z * it) }
val nearestCardinal: ForgeDirection get() = nearestAngle(this, forgeDirs.asIterable()) { it.vec }.first
}
/** 3D vector of [Int]s. Offers both mutable operations, and immutable operations in operator notation. */
data class Int3(var x: Int, var y: Int, var z: Int) {
constructor(dir: ForgeDirection) : this(dir.offsetX, dir.offsetY, dir.offsetZ)
constructor(offset: Pair<Int, ForgeDirection>) : this(
offset.first * offset.second.offsetX,
offset.first * offset.second.offsetY,
offset.first * offset.second.offsetZ
)
companion object {
val zero = Int3(0, 0, 0)
}
// immutable operations
operator fun plus(other: Int3) = Int3(x + other.x, y + other.y, z + other.z)
operator fun plus(other: Pair<Int, ForgeDirection>) = Int3(
x + other.first * other.second.offsetX,
y + other.first * other.second.offsetY,
z + other.first * other.second.offsetZ
)
operator fun unaryMinus() = Int3(-x, -y, -z)
operator fun minus(other: Int3) = Int3(x - other.x, y - other.y, z - other.z)
operator fun times(scale: Int) = Int3(x * scale, y * scale, z * scale)
operator fun times(other: Int3) = Int3(x * other.x, y * other.y, z * other.z)
/** Rotate this vector, and return coordinates in the unrotated frame */
fun rotate(rot: Rotation) = Int3(
rot.rotatedComponent(EAST, x, y, z),
rot.rotatedComponent(UP, x, y, z),
rot.rotatedComponent(SOUTH, x, y, z)
)
// mutable operations
fun setTo(other: Int3): Int3 { x = other.x; y = other.y; z = other.z; return this }
fun setTo(x: Int, y: Int, z: Int): Int3 { this.x = x; this.y = y; this.z = z; return this }
fun add(other: Int3): Int3 { x += other.x; y += other.y; z += other.z; return this }
fun sub(other: Int3): Int3 { x -= other.x; y -= other.y; z -= other.z; return this }
fun invert(): Int3 { x = -x; y = -y; z = -z; return this }
fun mul(scale: Int): Int3 { x *= scale; y *= scale; z *= scale; return this }
fun mul(other: Int3): Int3 { x *= other.x; y *= other.y; z *= other.z; return this }
fun rotateMut(rot: Rotation): Int3 {
val rotX = rot.rotatedComponent(EAST, x, y, z)
val rotY = rot.rotatedComponent(UP, x, y, z)
val rotZ = rot.rotatedComponent(SOUTH, x, y, z)
return setTo(rotX, rotY, rotZ)
}
}
// ================================
// Rotation
// ================================
val ForgeDirection.rotations: Array<ForgeDirection> get() =
Array(6) { idx -> ForgeDirection.values()[ForgeDirection.ROTATION_MATRIX[ordinal][idx]] }
fun ForgeDirection.rotate(rot: Rotation) = rot.forward[ordinal]
fun rot(axis: ForgeDirection) = Rotation.rot90[axis.ordinal]
/**
* Class representing an arbitrary rotation (or combination of rotations) around cardinal axes by 90 degrees.
* In effect, a permutation of [ForgeDirection]s.
*/
@Suppress("NOTHING_TO_INLINE")
class Rotation(val forward: Array<ForgeDirection>, val reverse: Array<ForgeDirection>) {
operator fun plus(other: Rotation) = Rotation(
Array(6) { idx -> forward[other.forward[idx].ordinal] },
Array(6) { idx -> other.reverse[reverse[idx].ordinal] }
)
operator fun unaryMinus() = Rotation(reverse, forward)
operator fun times(num: Int) = when(num % 4) { 1 -> this; 2 -> this + this; 3 -> -this; else -> identity }
inline fun rotatedComponent(dir: ForgeDirection, x: Int, y: Int, z: Int) =
when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0 }
inline fun rotatedComponent(dir: ForgeDirection, x: Double, y: Double, z: Double) =
when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0.0 }
companion object {
// Forge rotation matrix is left-hand
val rot90 = Array(6) { idx -> Rotation(forgeDirs[idx].opposite.rotations, forgeDirs[idx].rotations) }
val identity = Rotation(forgeDirs, forgeDirs)
}
}
// ================================
// Miscellaneous
// ================================
/** List of all 12 box edges, represented as a [Pair] of [ForgeDirection]s */
val boxEdges = forgeDirs.flatMap { face1 -> forgeDirs.filter { it.axis > face1.axis }.map { face1 to it } }
/**
* Get the closest object to the specified point from a list of objects.
*
* @param[vertex] the reference point
* @param[objs] list of geomertric objects
* @param[objPos] lambda to calculate the position of an object
* @return [Pair] of (object, distance)
*/
fun <T> nearestPosition(vertex: Double3, objs: Iterable<T>, objPos: (T)->Double3): Pair<T, Double> =
objs.map { it to (objPos(it) - vertex).length }.minBy { it.second }!!
/**
* Get the object closest in orientation to the specified vector from a list of objects.
*
* @param[vector] the reference vector (direction)
* @param[objs] list of geomertric objects
* @param[objAngle] lambda to calculate the orientation of an object
* @return [Pair] of (object, normalized dot product)
*/
fun <T> nearestAngle(vector: Double3, objs: Iterable<T>, objAngle: (T)->Double3): Pair<T, Double> =
objs.map { it to objAngle(it).dot(vector) }.maxBy { it.second }!!
data class FaceCorners(val topLeft: Pair<ForgeDirection, ForgeDirection>,
val topRight: Pair<ForgeDirection, ForgeDirection>,
val bottomLeft: Pair<ForgeDirection, ForgeDirection>,
val bottomRight: Pair<ForgeDirection, ForgeDirection>) {
constructor(top: ForgeDirection, left: ForgeDirection) :
this(top to left, top to left.opposite, top.opposite to left, top.opposite to left.opposite)
val asArray = arrayOf(topLeft, topRight, bottomLeft, bottomRight)
val asList = listOf(topLeft, topRight, bottomLeft, bottomRight)
}
val faceCorners = forgeDirs.map { when(it) {
DOWN -> FaceCorners(SOUTH, WEST)
UP -> FaceCorners(SOUTH, EAST)
NORTH -> FaceCorners(WEST, UP)
SOUTH -> FaceCorners(UP, WEST)
WEST -> FaceCorners(SOUTH, UP)
EAST ->FaceCorners(SOUTH, DOWN)
else -> FaceCorners(UNKNOWN, UNKNOWN)
}}

View File

@@ -0,0 +1,138 @@
package mods.octarinecore.client.render
import mods.octarinecore.minmax
import mods.octarinecore.replace
import net.minecraftforge.common.util.ForgeDirection
import java.lang.Math.*
/**
* Vertex UV coordinates
*
* Zero-centered: coordinates fall between (-0.5, 0.5) (inclusive)
*/
data class UV(val u: Double, val v: Double) {
companion object {
val topLeft = UV(-0.5, -0.5)
val topRight = UV(0.5, -0.5)
val bottomLeft = UV(-0.5, 0.5)
val bottomRight = UV(0.5, 0.5)
}
val rotate: UV get() = UV(v, -u)
fun rotate(n: Int) = when(n % 4) {
0 -> copy()
1 -> UV(v, -u)
2 -> UV(-u, -v)
else -> UV(-v, u)
}
fun clamp(minU: Double = -0.5, maxU: Double = 0.5, minV: Double = -0.5, maxV: Double = 0.5) =
UV(u.minmax(minU, maxU), v.minmax(minV, maxV))
fun mirror(mirrorU: Boolean, mirrorV: Boolean) = UV(if (mirrorU) -u else u, if (mirrorV) -v else v)
}
/**
* Model vertex
*
* @param[xyz] x, y, z coordinates
* @param[uv] u, v coordinates
* @param[aoShader] [Shader] instance to use with AO rendering
* @param[flatShader] [Shader] instance to use with non-AO rendering
*/
data class Vertex(val xyz: Double3 = Double3(0.0, 0.0, 0.0),
val uv: UV = UV(0.0, 0.0),
val aoShader: Shader = NoShader,
val flatShader: Shader = NoShader)
/**
* Model quad
*/
data class Quad(val v1: Vertex, val v2: Vertex, val v3: Vertex, val v4: Vertex) {
val verts = arrayOf(v1, v2, v3, v4)
inline fun transformV(trans: (Vertex)->Vertex): Quad = transformVI { vertex, idx -> trans(vertex) }
inline fun transformVI(trans: (Vertex, Int)->Vertex): Quad =
Quad(trans(v1, 0), trans(v2, 1), trans(v3, 2), trans(v4, 3))
val normal: Double3 get() = (v2.xyz - v1.xyz).cross(v4.xyz - v1.xyz).normalize
fun move(trans: Double3) = transformV { it.copy(xyz = it.xyz + trans) }
fun move(trans: Pair<Double, ForgeDirection>) = move(Double3(trans.second) * trans.first)
fun scale (scale: Double) = transformV { it.copy(xyz = it.xyz * scale) }
fun scale (scale: Double3) = transformV { it.copy(xyz = Double3(it.xyz.x * scale.x, it.xyz.y * scale.y, it.xyz.z * scale.z)) }
fun scaleUV (scale: Double) = transformV { it.copy(uv = UV(it.uv.u * scale, it.uv.v * scale)) }
fun rotate(rot: Rotation) = transformV {
it.copy(xyz = it.xyz.rotate(rot), aoShader = it.aoShader.rotate(rot), flatShader = it.flatShader.rotate(rot))
}
fun rotateUV(n: Int) = transformV { it.copy(uv = it.uv.rotate(n)) }
fun clampUV(minU: Double = -0.5, maxU: Double = 0.5, minV: Double = -0.5, maxV: Double = 0.5) =
transformV { it.copy(uv = it.uv.clamp(minU, maxU, minV, maxV)) }
fun mirrorUV(mirrorU: Boolean, mirrorV: Boolean) = transformV { it.copy(uv = it.uv.mirror(mirrorU, mirrorV)) }
fun setAoShader(resolver: (Quad, Vertex)->Shader, predicate: (Vertex, Int)->Boolean = { v, vi -> true }) =
transformVI { vertex, idx ->
if (!predicate(vertex, idx)) vertex else vertex.copy(aoShader = resolver(this@Quad, vertex))
}
fun setFlatShader(resolver: (Quad, Vertex)->Shader, predicate: (Vertex, Int)->Boolean = { v, vi -> true }) =
transformVI { vertex, idx ->
if (!predicate(vertex, idx)) vertex else vertex.copy(flatShader = resolver(this@Quad, vertex))
}
fun setFlatShader(shader: Shader) = transformVI { vertex, idx -> vertex.copy(flatShader = shader) }
val flipped: Quad get() = Quad(v4, v3, v2, v1)
}
/**
* Model. The basic unit of rendering blocks with OctarineCore.
*
* The model should be positioned so that (0,0,0) is the block center.
* The block extends to (-0.5, 0.5) in all directions (inclusive).
*/
class Model() {
constructor(other: List<Quad>) : this() { quads.addAll(other) }
val quads = linkedListOf<Quad>()
fun Quad.add() = quads.add(this)
fun Iterable<Quad>.addAll() = forEach { quads.add(it) }
fun transformQ(trans: (Quad)->Quad) = quads.replace(trans)
fun transformV(trans: (Vertex)->Vertex) = quads.replace{ it.transformV(trans) }
fun verticalRectangle(x1: Double, z1: Double, x2: Double, z2: Double, yBottom: Double, yTop: Double) = Quad(
Vertex(Double3(x1, yBottom, z1), UV.bottomLeft),
Vertex(Double3(x2, yBottom, z2), UV.bottomRight),
Vertex(Double3(x2, yTop, z2), UV.topRight),
Vertex(Double3(x1, yTop, z1), UV.topLeft)
)
fun horizontalRectangle(x1: Double, z1: Double, x2: Double, z2: Double, y: Double): Quad {
val xMin = min(x1, x2); val xMax = max(x1, x2)
val zMin = min(z1, z2); val zMax = max(z1, z2)
return Quad(
Vertex(Double3(xMin, y, zMin), UV.topLeft),
Vertex(Double3(xMin, y, zMax), UV.bottomLeft),
Vertex(Double3(xMax, y, zMax), UV.bottomRight),
Vertex(Double3(xMax, y, zMin), UV.topRight)
)
}
fun faceQuad(face: ForgeDirection): Quad {
val base = face.vec * 0.5
val top = faceCorners[face.ordinal].topLeft.first.vec * 0.5
val left = faceCorners[face.ordinal].topLeft.second.vec * 0.5
return Quad(
Vertex(base + top + left, UV.topLeft),
Vertex(base - top + left, UV.bottomLeft),
Vertex(base - top - left, UV.bottomRight),
Vertex(base + top - left, UV.topRight)
)
}
}
val fullCube = Model().apply {
forgeDirs.forEach {
faceQuad(it)
.setAoShader(faceOrientedAuto(corner = cornerAo(it.axis), edge = null))
.setFlatShader(faceOrientedAuto(corner = cornerFlat, edge = null))
.add()
}
}

View File

@@ -0,0 +1,138 @@
package mods.octarinecore.client.render
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.Tessellator
import net.minecraft.util.IIcon
import net.minecraftforge.common.util.ForgeDirection
import net.minecraftforge.common.util.ForgeDirection.*
class ModelRenderer() : ShadingContext() {
/** Holds final vertex data before it goes to the [Tessellator]. */
val temp = RenderVertex()
/**
* Render a [Model].
* The [blockContext] and [renderBlocks] need to be set up correctly, including first rendering the
* corresponding block to capture shading values!
*
* @param[model] model to render
* @param[rot] rotation to apply to the model
* @param[trans] translation to apply to the model
* @param[forceFlat] force flat shading even if AO is enabled
* @param[icon] lambda to resolve the texture to use for each quad
* @param[rotateUV] lambda to get amount of UV rotation for each quad
* @param[postProcess] lambda to perform arbitrary modifications on the [RenderVertex] just before it goes to the [Tessellator]
*/
inline fun render(
model: Model,
rot: Rotation,
trans: Double3 = blockContext.blockCenter,
forceFlat: Boolean = false,
icon: (ShadingContext, Int, Quad) -> IIcon,
rotateUV: (Quad) -> Int,
postProcess: RenderVertex.(ShadingContext, Int, Quad, Int, Vertex) -> Unit
) {
rotation = rot
aoEnabled = Minecraft.isAmbientOcclusionEnabled()
model.quads.forEachIndexed { quadIdx, quad ->
val drawIcon = icon(this, quadIdx, quad)
val uvRot = rotateUV(quad)
quad.verts.forEachIndexed { vertIdx, vert ->
temp.init(vert)
temp.rotate(rotation).translate(trans).rotateUV(uvRot).setIcon(drawIcon)
val shader = if (aoEnabled && !forceFlat) vert.aoShader else vert.flatShader
shader.shade(this, temp)
temp.postProcess(this, quadIdx, quad, vertIdx, vert)
Tessellator.instance.apply {
setBrightness(temp.brightness)
setColorOpaque_F(temp.red, temp.green, temp.blue)
addVertexWithUV(temp.x, temp.y, temp.z, temp.u, temp.v)
}
}
}
}
}
/**
* Queried by [Shader] objects to get rendering-relevant data of the current block in a rotated frame of reference.
*/
open class ShadingContext {
var rotation = Rotation.identity
var aoEnabled = Minecraft.isAmbientOcclusionEnabled()
fun aoShading(face: ForgeDirection, corner1: ForgeDirection, corner2: ForgeDirection) =
renderBlocks.capture.aoShading(face.rotate(rotation), corner1.rotate(rotation), corner2.rotate(rotation))
fun blockColor(offset: Int3) = blockContext.blockColor(offset.rotate(rotation))
fun blockBrightness(offset: Int3) = blockContext.blockBrightness(offset.rotate(rotation))
fun icon(face: ForgeDirection) = blockContext.icon(face.rotate(rotation))
}
/**
*
*/
@Suppress("NOTHING_TO_INLINE")
class RenderVertex() {
var x: Double = 0.0
var y: Double = 0.0
var z: Double = 0.0
var u: Double = 0.0
var v: Double = 0.0
var brightness: Int = 0
var red: Float = 0.0f
var green: Float = 0.0f
var blue: Float = 0.0f
fun init(vertex: Vertex, rot: Rotation, trans: Double3): RenderVertex {
val result = vertex.xyz.rotate(rot) + trans
x = result.x; y = result.y; z = result.z
return this
}
fun init(vertex: Vertex): RenderVertex {
x = vertex.xyz.x; y = vertex.xyz.y; z = vertex.xyz.z;
u = vertex.uv.u; v = vertex.uv.v
return this
}
fun translate(trans: Double3): RenderVertex { x += trans.x; y += trans.y; z += trans.z; return this }
fun rotate(rot: Rotation): RenderVertex {
if (rot === Rotation.identity) return this
val rotX = rot.rotatedComponent(EAST, x, y, z)
val rotY = rot.rotatedComponent(UP, x, y, z)
val rotZ = rot.rotatedComponent(SOUTH, x, y, z)
x = rotX; y = rotY; z = rotZ
return this
}
inline fun rotateUV(n: Int): RenderVertex {
when (n % 4) {
1 -> { val t = v; v = -u; u = t; return this }
2 -> { u = -u; v = -v; return this }
3 -> { val t = -v; v = u; u = t; return this }
else -> { return this }
}
}
inline fun setIcon(icon: IIcon): RenderVertex {
u = (icon.maxU - icon.minU) * (u + 0.5) + icon.minU
v = (icon.maxV - icon.minV) * (v + 0.5) + icon.minV
return this
}
inline fun setGrey(level: Float) {
val grey = Math.min((red + green + blue) * 0.333f * level, 1.0f)
red = grey; green = grey; blue = grey
}
inline fun multiplyColor(color: Int) {
red *= (color shr 16 and 255) / 256.0f
green *= (color shr 8 and 255) / 256.0f
blue *= (color and 255) / 256.0f
}
inline fun setColor(color: Int) {
red = (color shr 16 and 255) / 256.0f
green = (color shr 8 and 255) / 256.0f
blue = (color and 255) / 256.0f
}
}
/** Perform no post-processing */
val noPost: RenderVertex.(ShadingContext, Int, Quad, Int, Vertex) -> Unit = { ctx, qi, q, vi, v -> }

View File

@@ -0,0 +1,68 @@
package mods.octarinecore.client.render
import mods.octarinecore.minmax
import net.minecraft.world.IBlockAccess
import net.minecraftforge.common.util.ForgeDirection
/**
* Delegating [IBlockAccess] that fakes a _modified_ location to return values from a _target_ location.
* All other locations are handled normally.
*
* @param[original] the [IBlockAccess] that is delegated to
* @param[xModded] x coordinate of the _modified_ location
* @param[yModded] y coordinate of the _modified_ location
* @param[zModded] z coordinate of the _modified_ location
* @param[xTarget] x coordinate of the _target_ location
* @param[yTarget] y coordinate of the _target_ location
* @param[zTarget] z coordinate of the _target_ location
*/
class OffsetBlockAccess(val original: IBlockAccess,
@JvmField val xModded: Int, @JvmField val yModded: Int, @JvmField val zModded: Int,
@JvmField val xTarget: Int, @JvmField val yTarget: Int, @JvmField val zTarget: Int) : IBlockAccess {
inline fun <reified T> withOffset(x: Int, y: Int, z: Int, func: (Int,Int,Int)->T): T {
if (x == xModded && y == yModded && z == zModded) {
return func(xTarget, yTarget, zTarget)
} else {
return func(x, y, z)
}
}
override fun getBlock(x: Int, y: Int, z: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.getBlock(xAct, yAct, zAct) }
override fun getBlockMetadata(x: Int, y: Int, z: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.getBlockMetadata(xAct, yAct, zAct) }
override fun getTileEntity(x: Int, y: Int, z: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.getTileEntity(xAct, yAct, zAct) }
override fun isSideSolid(x: Int, y: Int, z: Int, side: ForgeDirection?, _default: Boolean) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.isSideSolid(xAct, yAct, zAct, side, _default) }
override fun isAirBlock(x: Int, y: Int, z: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.isAirBlock(xAct, yAct, zAct) }
override fun getLightBrightnessForSkyBlocks(x: Int, y: Int, z: Int, side: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.getLightBrightnessForSkyBlocks(xAct, yAct, zAct, side) }
override fun isBlockProvidingPowerTo(x: Int, y: Int, z: Int, side: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.isBlockProvidingPowerTo(xAct, yAct, zAct, side) }
override fun getBiomeGenForCoords(x: Int, z: Int) = withOffset(x, 0, z)
{ xAct, yAct, zAct -> original.getBiomeGenForCoords(xAct, zAct) }
override fun getHeight() = original.height
override fun extendedLevelsInChunkCache() = original.extendedLevelsInChunkCache()
}
/**
* Temporarily replaces the [IBlockAccess] used by this [BlockContext] and the corresponding [ExtendedRenderBlocks]
* to use an [OffsetBlockAccess] while executing this lambda.
*
* @param[modded] the _modified_ location
* @param[target] the _target_ location
* @param[func] the lambda to execute
*/
inline fun <reified T> BlockContext.withOffset(modded: Int3, target: Int3, func: () -> T): T {
val original = world!!
world = OffsetBlockAccess(original, x + modded.x, y + modded.y, z + modded.z, x + target.x, y + target.y, z + target.z)
renderBlocks.blockAccess = world
val result = func()
world = original
renderBlocks.blockAccess = original
return result
}

View File

@@ -0,0 +1,56 @@
@file:JvmName("PixelFormat")
package mods.octarinecore.client.render
import java.awt.Color
/** List of bit-shift offsets in packed brightness values where meaningful (4-bit) data is contained. */
var brightnessComponents = listOf(20, 4)
/** Multiply the components of this packed brightness value with the given [Float]. */
infix fun Int.brMul(f: Float): Int {
val weight = (f * 256.0f).toInt()
var result = 0
brightnessComponents.forEach { shift ->
val raw = (this shr shift) and 15
val weighted = (raw) * weight / 256
result = result or (weighted shl shift)
}
return result
}
/** Multiply the components of this packed color value with the given [Float]. */
infix fun Int.colorMul(f: Float): Int {
val weight = (f * 256.0f).toInt()
val red = (this shr 16 and 255) * weight / 256
val green = (this shr 8 and 255) * weight / 256
val blue = (this and 255) * weight / 256
return (red shl 16) or (green shl 8) or blue
}
/** Sum the components of all packed brightness values given. */
fun brSum(multiplier: Float?, vararg brightness: Int): Int {
val sum = Array(brightnessComponents.size) { 0 }
brightnessComponents.forEachIndexed { idx, shift -> brightness.forEach { br ->
val comp = (br shr shift) and 15
sum[idx] += comp
} }
var result = 0
brightnessComponents.forEachIndexed { idx, shift ->
val comp = if (multiplier == null)
((sum[idx]) shl shift)
else
((sum[idx].toFloat() * multiplier).toInt() shl shift)
result = result or comp
}
return result
}
data class HSB(var hue: Float, var saturation: Float, var brightness: Float) {
companion object {
fun fromColor(color: Int): HSB {
val hsbVals = Color.RGBtoHSB((color shr 16) and 255, (color shr 8) and 255, color and 255, null)
return HSB(hsbVals[0], hsbVals[1], hsbVals[2])
}
}
val asColor: Int get() = Color.HSBtoRGB(hue, saturation, brightness)
}

View File

@@ -0,0 +1,147 @@
package mods.octarinecore.client.render
import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler
import cpw.mods.fml.client.registry.RenderingRegistry
import mods.octarinecore.metaprog.reflectField
import mods.octarinecore.metaprog.reflectStaticField
import net.minecraft.block.Block
import net.minecraft.client.renderer.RenderBlocks
import net.minecraft.util.IIcon
import net.minecraftforge.common.util.ForgeDirection
import net.minecraftforge.common.util.ForgeDirection.*
/** Reference to the handler list in Forge [RenderingRegistry]. */
val renderingHandlers: Map<Int, ISimpleBlockRenderingHandler> = RenderingRegistry::class.java
.reflectStaticField<RenderingRegistry>("INSTANCE")!!
.reflectField<Map<Int, ISimpleBlockRenderingHandler>>("blockRenderers")!!
/**
* Used instead of the vanilla [RenderBlocks] to get to the AO values and textures used in rendering
* without duplicating vanilla code.
*/
class ExtendedRenderBlocks : RenderBlocks() {
/** Captures the AO values and textures used in a specific rendering pass when rendering a block. */
val capture = ShadingCapture()
override fun renderFaceXPos(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(EAST, block, x, y, z, icon)
override fun renderFaceXNeg(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(WEST, block, x, y, z, icon)
override fun renderFaceYPos(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(UP, block, x, y, z, icon)
override fun renderFaceYNeg(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(DOWN, block, x, y, z, icon)
override fun renderFaceZPos(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(SOUTH, block, x, y, z, icon)
override fun renderFaceZNeg(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(NORTH, block, x, y, z, icon)
/**
* Render a block face, saving relevant data if appropriate.
*/
@Suppress("NON_EXHAUSTIVE_WHEN")
fun renderFace(face: ForgeDirection, block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) {
if (capture.isCorrectPass(face)) {
saveAllShading(face); capture.icons[face.ordinal] = icon
}
if (capture.renderCallback(capture, face, capture.passes[face.ordinal], icon)) when (face) {
EAST -> super.renderFaceXPos(block, x, y, z, icon)
WEST -> super.renderFaceXNeg(block, x, y, z, icon)
UP -> super.renderFaceYPos(block, x, y, z, icon)
DOWN -> super.renderFaceYNeg(block, x, y, z, icon)
SOUTH -> super.renderFaceZPos(block, x, y, z, icon)
NORTH -> super.renderFaceZNeg(block, x, y, z, icon)
}
}
fun saveTopLeft(face: ForgeDirection, corner: Pair<ForgeDirection, ForgeDirection>) =
capture.aoShading(face, corner.first, corner.second)
.set(brightnessTopLeft, colorRedTopLeft, colorGreenTopLeft, colorBlueTopLeft)
fun saveTopRight(face: ForgeDirection, corner: Pair<ForgeDirection, ForgeDirection>) =
capture.aoShading(face, corner.first, corner.second)
.set(brightnessTopRight, colorRedTopRight, colorGreenTopRight, colorBlueTopRight)
fun saveBottomLeft(face: ForgeDirection, corner: Pair<ForgeDirection, ForgeDirection>) =
capture.aoShading(face, corner.first, corner.second)
.set(brightnessBottomLeft, colorRedBottomLeft, colorGreenBottomLeft, colorBlueBottomLeft)
fun saveBottomRight(face: ForgeDirection, corner: Pair<ForgeDirection, ForgeDirection>) =
capture.aoShading(face, corner.first, corner.second)
.set(brightnessBottomRight, colorRedBottomRight, colorGreenBottomRight, colorBlueBottomRight)
fun saveAllShading(face: ForgeDirection) {
saveTopLeft(face, faceCorners[face.ordinal].topLeft)
saveTopRight(face, faceCorners[face.ordinal].topRight)
saveBottomLeft(face, faceCorners[face.ordinal].bottomLeft)
saveBottomRight(face, faceCorners[face.ordinal].bottomRight)
}
}
/**
* Captures the AO values and textures used in a specific rendering pass when rendering a block.
*/
class ShadingCapture {
/** Sparse array of stored AO data. */
val aoShadings = arrayOfNulls<AoData>(6 * 6 * 6)
/** List of stored AO data (only valid instances). */
var shadingsList = listOf<AoData>()
/** List of stored texture data. */
val icons = arrayOfNulls<IIcon>(6)
/** Number of passes to go on a given face. */
val passes = Array(6) { 0 }
/** lambda to determine which faces to render. */
var renderCallback = alwaysRender
init {
(0..5).forEach { i1 ->
(0..5).forEach { i2 ->
(i2..5).forEach { i3 ->
aoShadings[cornerId(i1, i2, i3)] = AoData()
}
}
}
shadingsList = aoShadings.filterNotNull()
}
/**
* Get the AO data of a specific corner.
*
* The two corner directions are interchangeable. All 3 parameters must lie on different axes.
*
* @param[face] block face
* @param[corner1] first direction of corner on face
* @param[corner2] second direction of corner on face
*/
fun aoShading(face: ForgeDirection, corner1: ForgeDirection, corner2: ForgeDirection) =
aoShadings[cornerId(face, corner1, corner2)]!!
/** Returns true if the AO and texture data should be saved. Mutates state. */
fun isCorrectPass(face: ForgeDirection) = (passes[face.ordinal]-- > 0)
/**
* Reset all data and pass counters.
*
* @param[targetPass] which render pass to save
*/
fun reset(targetPass: Int) {
shadingsList.forEach { it.reset() }
(0..5).forEach { idx -> icons[idx] = null; passes[idx] = targetPass }
}
/** One-dimensional index of a specific corner. */
protected fun cornerId(face: Int, corner1: Int, corner2: Int) = when (corner2 > corner1) {
true -> 36 * face + 6 * corner1 + corner2
false -> 36 * face + 6 * corner2 + corner1
}
/** One-dimensional index of a specific corner. */
protected fun cornerId(face: ForgeDirection, corner1: ForgeDirection, corner2: ForgeDirection) =
cornerId(face.ordinal, corner1.ordinal, corner2.ordinal)
}
/** Lambda to render all faces of a block */
val alwaysRender: (ShadingCapture, ForgeDirection, Int, IIcon?) -> Boolean = { ctx, face, pass, icon -> true }
/** Lambda to render no faces of a block */
val neverRender: (ShadingCapture, ForgeDirection, Int, IIcon?) -> Boolean = { ctx, face, pass, icon -> false }

View File

@@ -0,0 +1,148 @@
package mods.octarinecore.client.render
import net.minecraftforge.common.util.ForgeDirection
const val defaultCornerDimming = 0.5f
const val defaultEdgeDimming = 0.8f
// ================================
// Resolvers for automatic shading
// ================================
fun cornerAo(fallbackAxis: Axis): (ForgeDirection, ForgeDirection, ForgeDirection)->Shader = { face, dir1, dir2 ->
val fallbackDir = listOf(face, dir1, dir2).find { it.axis == fallbackAxis }!!
CornerSingleFallback(face, dir1, dir2, fallbackDir)
}
val cornerFlat = { face: ForgeDirection, dir1: ForgeDirection, dir2: ForgeDirection -> FaceFlat(face) }
fun cornerAoTri(func: (AoData, AoData)-> AoData) = { face: ForgeDirection, dir1: ForgeDirection, dir2: ForgeDirection ->
CornerTri(face, dir1, dir2, func)
}
val cornerAoMaxGreen = cornerAoTri { s1, s2 -> if (s1.green > s2.green) s1 else s2 }
fun cornerInterpolate(edgeAxis: Axis, weight: Float, dimming: Float): (ForgeDirection, ForgeDirection, ForgeDirection)->Shader = { dir1, dir2, dir3 ->
val edgeDir = listOf(dir1, dir2, dir3).find { it.axis == edgeAxis }!!
val faceDirs = listOf(dir1, dir2, dir3).filter { it.axis != edgeAxis }
CornerInterpolateDimming(faceDirs[0], faceDirs[1], edgeDir, weight, dimming)
}
// ================================
// Shaders
// ================================
object NoShader : Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) = vertex.shade(AoData.black)
override fun rotate(rot: Rotation) = this
}
class CornerSingleFallback(val face: ForgeDirection, val dir1: ForgeDirection, val dir2: ForgeDirection, val fallbackDir: ForgeDirection, val fallbackDimming: Float = defaultCornerDimming) : Shader {
val offset = Int3(fallbackDir)
override fun shade(context: ShadingContext, vertex: RenderVertex) {
val shading = context.aoShading(face, dir1, dir2)
if (shading.valid)
vertex.shade(shading)
else
vertex.shade(context.blockBrightness(offset) brMul fallbackDimming, context.blockColor(offset) colorMul fallbackDimming)
}
override fun rotate(rot: Rotation) = CornerSingleFallback(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), fallbackDir.rotate(rot), fallbackDimming)
}
inline fun accumulate(v1: AoData?, v2: AoData?, func: ((AoData, AoData)-> AoData)): AoData? {
val v1ok = v1 != null && v1.valid
val v2ok = v2 != null && v2.valid
if (v1ok && v2ok) return func(v1!!, v2!!)
if (v1ok) return v1
if (v2ok) return v2
return null
}
class CornerTri(val face: ForgeDirection, val dir1: ForgeDirection, val dir2: ForgeDirection,
val func: ((AoData, AoData)-> AoData)) : Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
var acc = accumulate(
context.aoShading(face, dir1, dir2),
context.aoShading(dir1, face, dir2),
func)
acc = accumulate(
acc,
context.aoShading(dir2, face, dir1),
func)
vertex.shade(acc ?: AoData.black)
}
override fun rotate(rot: Rotation) = CornerTri(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), func)
}
class EdgeInterpolateFallback(val face: ForgeDirection, val edgeDir: ForgeDirection, val pos: Double, val fallbackDimming: Float = defaultEdgeDimming): Shader {
val offset = Int3(edgeDir)
val edgeAxis = axes.find { it != face.axis && it != edgeDir.axis }!!
val weightN = (0.5 - pos).toFloat()
val weightP = (0.5 + pos).toFloat()
override fun shade(context: ShadingContext, vertex: RenderVertex) {
val shadingP = context.aoShading(face, edgeDir, (edgeAxis to Dir.P).face)
val shadingN = context.aoShading(face, edgeDir, (edgeAxis to Dir.N).face)
if (!shadingP.valid && !shadingN.valid)
return vertex.shade(context.blockBrightness(offset) brMul fallbackDimming, context.blockColor(offset) colorMul fallbackDimming)
if (!shadingP.valid) return vertex.shade(shadingN)
if (!shadingN.valid) return vertex.shade(shadingP)
vertex.shade(shadingP, shadingN, weightP, weightN)
}
override fun rotate(rot: Rotation) = EdgeInterpolateFallback(face.rotate(rot), edgeDir.rotate(rot), pos)
}
class CornerInterpolateDimming(val face1: ForgeDirection, val face2: ForgeDirection, val edgeDir: ForgeDirection,
val weight: Float, val dimming: Float, val fallbackDimming: Float = defaultCornerDimming) : Shader {
val offset = Int3(edgeDir)
override fun shade(context: ShadingContext, vertex: RenderVertex) {
var shading1 = context.aoShading(face1, edgeDir, face2)
var shading2 = context.aoShading(face2, edgeDir, face1)
var weight1 = weight
var weight2 = 1.0f - weight
if (!shading1.valid && !shading2.valid)
return vertex.shade(context.blockBrightness(offset) brMul fallbackDimming, context.blockColor(offset) colorMul fallbackDimming)
if (!shading1.valid) { shading1 = shading2; weight1 *= dimming }
if (!shading2.valid) { shading2 = shading1; weight2 *= dimming }
vertex.shade(shading1, shading2, weight1, weight2)
}
override fun rotate(rot: Rotation) =
CornerInterpolateDimming(face1.rotate(rot), face2.rotate(rot), edgeDir.rotate(rot), weight, dimming, fallbackDimming)
}
class FaceCenter(val face: ForgeDirection): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
vertex.red = 0.0f; vertex.green = 0.0f; vertex.blue = 0.0f;
val b = IntArray(4)
faceCorners[face.ordinal].asList.forEachIndexed { idx, corner ->
val shading = context.aoShading(face, corner.first, corner.second)
vertex.red += shading.red
vertex.green += shading.green
vertex.blue += shading.blue
b[idx] = shading.brightness
}
vertex.apply { red *= 0.25f; green *= 0.25f; blue *= 0.25f }
vertex.brightness = brSum(0.25f, *b)
}
override fun rotate(rot: Rotation) = FaceCenter(face.rotate(rot))
}
class FaceFlat(val face: ForgeDirection): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
val color = context.blockColor(Int3.zero)
vertex.shade(context.blockBrightness(face.offset), color)
}
override fun rotate(rot: Rotation): Shader = FaceFlat(face.rotate(rot))
}
class FlatOffset(val offset: Int3): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
vertex.brightness = context.blockBrightness(offset)
vertex.setColor(context.blockColor(offset))
}
override fun rotate(rot: Rotation): Shader = this
}
class FlatOffsetNoColor(val offset: Int3): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
vertex.brightness = context.blockBrightness(offset)
vertex.red = 1.0f; vertex.green = 1.0f; vertex.blue = 1.0f
}
override fun rotate(rot: Rotation): Shader = this
}

View File

@@ -0,0 +1,131 @@
package mods.octarinecore.client.render
import net.minecraftforge.common.util.ForgeDirection
import java.lang.Math.*
/** Holds shading values for block corners as calculated by vanilla Minecraft rendering. */
class AoData() {
var valid = false
var brightness = 0
var red: Float = 0.0f
var green: Float = 0.0f
var blue: Float = 0.0f
fun reset() { valid = false }
fun set(brightness: Int, red: Float, green: Float, blue: Float) {
if (valid) return
this.valid = true
this.brightness = brightness
this.red = red
this.green = green
this.blue = blue
}
companion object {
val black = AoData();
}
}
/**
* Instances of this interface are associated with [Model] vertices, and used to apply brightness and color
* values to a [RenderVertex].
*/
interface Shader {
/**
* Set shading values of a [RenderVertex]
*
* @param[context] context that can be queried for shading data in a [Model]-relative frame of reference
* @param[vertex] the [RenderVertex] to manipulate
*/
fun shade(context: ShadingContext, vertex: RenderVertex)
/**
* Return a new rotated version of this [Shader]. Used during [Model] setup when rotating the model itself.
*/
fun rotate(rot: Rotation): Shader
/** Set all shading values on the [RenderVertex] to match the given [AoData]. */
fun RenderVertex.shade(shading: AoData) {
brightness = shading.brightness; red = shading.red; green = shading.green; blue = shading.blue
}
/** Set the shading values on the [RenderVertex] to a weighted average of the two [AoData] instances. */
fun RenderVertex.shade(shading1: AoData, shading2: AoData, weight1: Float = 0.5f, weight2: Float = 0.5f) {
red = min(shading1.red * weight1 + shading2.red * weight2, 1.0f)
green = min(shading1.green * weight1 + shading2.green * weight2, 1.0f)
blue = min(shading1.blue * weight1 + shading2.blue * weight2, 1.0f)
brightness = brSum(null, shading1.brightness brMul weight1, shading2.brightness brMul weight2)
}
/**
* Set the shading values on the [RenderVertex] directly.
*
* @param[brightness] packed brightness value
* @param[color] packed color value
*/
fun RenderVertex.shade(brightness: Int, color: Int) {
this.brightness = brightness; setColor(color)
}
}
/**
* Returns a shader resolver for quads that point towards one of the 6 block faces.
* The resolver works the following way:
* - determines which face the _quad_ normal points towards (if not overridden)
* - determines the distance of the _vertex_ to the corners and edge midpoints on that block face
* - if _corner_ is given, and the _vertex_ is closest to a block corner, returns the [Shader] created by _corner_
* - if _edge_ is given, and the _vertex_ is closest to an edge midpoint, returns the [Shader] created by _edge_
*
* @param[overrideFace] assume the given face instead of going by the _quad_ normal
* @param[corner] shader instantiation lambda for corner vertices
* @param[edge] shader instantiation lambda for edge midpoint vertices
*/
fun faceOrientedAuto(overrideFace: ForgeDirection? = null,
corner: ((ForgeDirection, ForgeDirection, ForgeDirection)->Shader)? = null,
edge: ((ForgeDirection, ForgeDirection)->Shader)? = null) =
fun(quad: Quad, vertex: Vertex): Shader {
val quadFace = overrideFace ?: quad.normal.nearestCardinal
val nearestCorner = nearestPosition(vertex.xyz, faceCorners[quadFace.ordinal].asList) {
(quadFace.vec + it.first.vec + it.second.vec) * 0.5
}
val nearestEdge = nearestPosition(vertex.xyz, quadFace.perpendiculars) {
(quadFace.vec + it.vec) * 0.5
}
if (edge != null && (nearestEdge.second < nearestCorner.second || corner == null))
return edge(quadFace, nearestEdge.first)
else return corner!!(quadFace, nearestCorner.first.first, nearestCorner.first.second)
}
/**
* Returns a shader resolver for quads that point towards one of the 12 block edges.
* The resolver works the following way:
* - determines which edge the _quad_ normal points towards (if not overridden)
* - determines which face midpoint the _vertex_ is closest to, of the 2 block faces that share this edge
* - determines which block corner _of this face_ the _vertex_ is closest to
* - returns the [Shader] created by _corner_
*
* @param[overrideEdge] assume the given edge instead of going by the _quad_ normal
* @param[corner] shader instantiation lambda
*/
fun edgeOrientedAuto(overrideEdge: Pair<ForgeDirection, ForgeDirection>? = null,
corner: (ForgeDirection, ForgeDirection, ForgeDirection)->Shader) =
fun(quad: Quad, vertex: Vertex): Shader {
val edgeDir = overrideEdge ?: nearestAngle(quad.normal, boxEdges) { it.first.vec + it.second.vec }.first
val nearestFace = nearestPosition(vertex.xyz, edgeDir.toList()) { it.vec }.first
val nearestCorner = nearestPosition(vertex.xyz, faceCorners[nearestFace.ordinal].asList) {
(nearestFace.vec + it.first.vec + it.second.vec) * 0.5
}.first
return corner(nearestFace, nearestCorner.first, nearestCorner.second)
}
fun faceOrientedInterpolate(overrideFace: ForgeDirection? = null) =
fun(quad: Quad, vertex: Vertex): Shader {
val resolver = faceOrientedAuto(overrideFace, edge = { face, edgeDir ->
val axis = axes.find { it != face.axis && it != edgeDir.axis }!!
val vec = Double3((axis to Dir.P).face)
val pos = vertex.xyz.dot(vec)
EdgeInterpolateFallback(face, edgeDir, pos)
})
return resolver(quad, vertex)
}

View File

@@ -0,0 +1,33 @@
package mods.octarinecore.client.resource
import java.awt.image.BufferedImage
import java.lang.Math.*
class CenteringTextureGenerator(domain: String, val aspectWidth: Int, val aspectHeight: Int) : TextureGenerator(domain) {
override fun generate(params: ParameterList): BufferedImage? {
val target = targetResource(params)!!
val baseTexture = resourceManager[target.second]?.loadImage() ?: return null
val frameWidth = baseTexture.width
val frameHeight = baseTexture.width * aspectHeight / aspectWidth
val frames = baseTexture.height / frameHeight
val size = max(frameWidth, frameHeight)
val resultTexture = BufferedImage(size, size * frames, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = resultTexture.createGraphics()
// iterate all frames
for (frame in 0 .. frames - 1) {
val baseFrame = baseTexture.getSubimage(0, size * frame, frameWidth, frameHeight)
val resultFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR)
resultFrame.createGraphics().apply {
drawImage(baseFrame, (size - frameWidth) / 2, (size - frameHeight) / 2, null)
}
graphics.drawImage(resultFrame, 0, size * frame, null)
}
return resultTexture
}
}

View File

@@ -0,0 +1,126 @@
package mods.octarinecore.client.resource
import cpw.mods.fml.client.FMLClientHandler
import mods.betterfoliage.loader.Refs
import mods.octarinecore.metaprog.reflectField
import net.minecraft.client.resources.IResourcePack
import net.minecraft.client.resources.data.IMetadataSerializer
import net.minecraft.client.resources.data.PackMetadataSection
import net.minecraft.util.ChatComponentText
import net.minecraft.util.ResourceLocation
import java.io.InputStream
import java.util.*
/**
* [IResourcePack] containing generated resources. Adds itself to the default resource pack list
* of Minecraft, so it is invisible and always active.
*
* @param[name] Name of the resource pack
* @param[generators] List of resource generators
*/
class GeneratorPack(val name: String, vararg val generators: GeneratorBase) : IResourcePack {
init {
// add to the default resource packs
FMLClientHandler.instance().reflectField<MutableList<IResourcePack>>("resourcePackList")!!.add(this)
}
override fun getPackName() = name
override fun getPackImage() = null
override fun getResourceDomains() = HashSet(generators.map { it.domain })
override fun getPackMetadata(serializer: IMetadataSerializer?, type: String?) =
if (type == "pack") PackMetadataSection(ChatComponentText("Generated resources"), 1) else null
override fun resourceExists(location: ResourceLocation?): Boolean =
if (location == null) false
else generators.find {
it.domain == location.resourceDomain && it.resourceExists(location)
} != null
override fun getInputStream(location: ResourceLocation?): InputStream? =
if (location == null) null
else generators.filter {
it.domain == location.resourceDomain && it.resourceExists(location)
}.map { it.getInputStream(location) }
.filterNotNull().first()
operator fun get(location: ResourceLocation?) = getInputStream(location)
}
/**
* Abstract base class for resource generators
*
* @param[domain] Resource domain of generator
*/
abstract class GeneratorBase(val domain: String) {
/** @see [IResourcePack.resourceExists] */
abstract fun resourceExists(location: ResourceLocation?): Boolean
/** @see [IResourcePack.getInputStream] */
abstract fun getInputStream(location: ResourceLocation?): InputStream?
}
/**
* Collection of named [String]-valued key-value pairs, with an extra unnamed (keyless) value.
* Meant to be encoded as a pipe-delimited list, and used as a [ResourceLocation] path
* to parametrized generated resources.
*
* @param[params] key-value pairs
* @param[value] keyless extra value
*/
class ParameterList(val params: Map<String, String>, val value: String?) {
override fun toString() =
params.entries
.sortedBy { it.key }
.fold("") { result, entry -> result + "|${entry.key}=${entry.value}"} +
(value?.let { "|$it" } ?: "")
/** Return the value of the given parameter. */
operator fun get(key: String) = params[key]
/** Check if the given parameter exists in this list. */
operator fun contains(key: String) = key in params
/** Return a new [ParameterList] with the given key-value pair appended to it. */
operator fun plus(pair: Pair<String, String>) = ParameterList(params + pair, this.value)
companion object {
/**
* Recreate the parameter list from the encoded string, i.e. the opposite of [toString].
*
* Everything before the first pipe character is dropped, so the decoding works even if
* something is prepended to the list (like _textures/blocks/_)
*/
fun fromString(input: String): ParameterList {
val params = hashMapOf<String, String>()
var value: String? = null
val slices = input.dropWhile { it != '|'}.split('|')
slices.forEach {
if (it.contains('=')) {
val keyValue = it.split('=')
if (keyValue.size == 2) params.put(keyValue[0], keyValue[1])
} else value = it
}
return ParameterList(params, value)
}
}
}
/**
* [GeneratorBase] returning parametrized generated resources.
*
* @param[domain] Resource domain of generator.
*/
abstract class ParameterBasedGenerator(domain: String) : GeneratorBase(domain) {
/** @see [IResourcePack.resourceExists] */
abstract fun resourceExists(params: ParameterList): Boolean
/** @see [IResourcePack.getInputStream] */
abstract fun getInputStream(params: ParameterList): InputStream?
override fun resourceExists(location: ResourceLocation?) =
resourceExists(ParameterList.fromString(location?.resourcePath ?: ""))
override fun getInputStream(location: ResourceLocation?) =
getInputStream(ParameterList.fromString(location?.resourcePath ?: ""))
}

View File

@@ -0,0 +1,126 @@
package mods.octarinecore.client.resource
import cpw.mods.fml.client.event.ConfigChangedEvent
import cpw.mods.fml.common.FMLCommonHandler
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import mods.octarinecore.client.render.Double3
import mods.octarinecore.client.render.Int3
import mods.octarinecore.client.render.Model
import net.minecraft.client.renderer.texture.IIconRegister
import net.minecraft.util.IIcon
import net.minecraft.util.MathHelper
import net.minecraft.util.ResourceLocation
import net.minecraft.world.World
import net.minecraft.world.gen.NoiseGeneratorSimplex
import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.event.world.WorldEvent
import java.util.*
// ============================
// Resource types
// ============================
interface IStitchListener { fun onStitch(atlas: IIconRegister) }
interface IConfigChangeListener { fun onConfigChange() }
interface IWorldLoadListener { fun onWorldLoad(world: World) }
/**
* Base class for declarative resource handling.
*
* Resources are automatically reloaded/recalculated when the appropriate events are fired.
*
* @param[modId] mod ID associated with this handler (used to filter config change events)
*/
open class ResourceHandler(val modId: String) {
val resources = linkedListOf<Any>()
open fun afterStitch() {}
// ============================
// Self-registration
// ============================
init {
MinecraftForge.EVENT_BUS.register(this)
FMLCommonHandler.instance().bus().register(this)
}
// ============================
// Resource declarations
// ============================
fun iconStatic(domain: String, path: String) = IconHolder(domain, path).apply { resources.add(this) }
fun iconStatic(location: ResourceLocation) = iconStatic(location.resourceDomain, location.resourcePath)
fun iconSet(domain: String, pathPattern: String) = IconSet(domain, pathPattern).apply { resources.add(this) }
fun iconSet(location: ResourceLocation) = iconSet(location.resourceDomain, location.resourcePath)
fun model(init: Model.()->Unit) = ModelHolder(init).apply { resources.add(this) }
fun modelSet(num: Int, init: Model.(Int)->Unit) = ModelSet(num, init).apply { resources.add(this) }
fun vectorSet(num: Int, init: (Int)-> Double3) = VectorSet(num, init).apply { resources.add(this) }
fun simplexNoise() = SimplexNoise().apply { resources.add(this) }
// ============================
// Event registration
// ============================
@SubscribeEvent
fun onStitch(event: TextureStitchEvent.Pre) {
if (event.map.textureType == 0) {
resources.forEach { (it as? IStitchListener)?.onStitch(event.map) }
afterStitch()
}
}
@SubscribeEvent
fun handleConfigChange(event: ConfigChangedEvent.OnConfigChangedEvent) {
if (event.modID == modId) resources.forEach { (it as? IConfigChangeListener)?.onConfigChange() }
}
@SubscribeEvent
fun handleWorldLoad(event: WorldEvent.Load) =
resources.forEach { (it as? IWorldLoadListener)?.onWorldLoad(event.world) }
}
// ============================
// Resource container classes
// ============================
class IconHolder(val domain: String, val name: String) : IStitchListener {
var icon: IIcon? = null
override fun onStitch(atlas: IIconRegister) { icon = atlas.registerIcon("$domain:$name") }
}
class ModelHolder(val init: Model.()->Unit): IConfigChangeListener {
var model: Model = Model().apply(init)
override fun onConfigChange() { model = Model().apply(init) }
}
class IconSet(val domain: String, val namePattern: String) : IStitchListener {
val icons = arrayOfNulls<IIcon>(16)
var num = 0
override fun onStitch(atlas: IIconRegister) {
num = 0;
(0..15).forEach { idx ->
val locReal = ResourceLocation(domain, "textures/blocks/${namePattern.format(idx)}.png")
if (resourceManager[locReal] != null) icons[num++] = atlas.registerIcon("$domain:${namePattern.format(idx)}")
}
}
operator fun get(idx: Int) = if (num == 0) null else icons[idx % num]
}
class ModelSet(val num: Int, val init: Model.(Int)->Unit): IConfigChangeListener {
val models = Array(num) { Model().apply{ init(it) } }
override fun onConfigChange() { (0..num-1).forEach { models[it] = Model().apply{ init(it) } } }
operator fun get(idx: Int) = models[idx % num]
}
class VectorSet(val num: Int, val init: (Int)->Double3): IConfigChangeListener {
val models = Array(num) { init(it) }
override fun onConfigChange() { (0..num-1).forEach { models[it] = init(it) } }
operator fun get(idx: Int) = models[idx % num]
}
class SimplexNoise() : IWorldLoadListener {
var noise = NoiseGeneratorSimplex()
override fun onWorldLoad(world: World) { noise = NoiseGeneratorSimplex(Random(world.worldInfo.seed))
}
operator fun get(x: Int, z: Int) = MathHelper.floor_double((noise.func_151605_a(x.toDouble(), z.toDouble()) + 1.0) * 32.0)
operator fun get(pos: Int3) = get(pos.x, pos.z)
}

View File

@@ -0,0 +1,88 @@
package mods.octarinecore.client.resource
import mods.octarinecore.client.resource.ResourceType.*
import net.minecraft.client.resources.IResource
import net.minecraft.util.ResourceLocation
import java.awt.image.BufferedImage
import java.io.InputStream
/** Type of generated texture resource */
enum class ResourceType {
COLOR, // regular diffuse map
METADATA, // texture metadata
NORMAL, // ShadersMod normal map
SPECULAR // ShadersMod specular map
}
/**
* Generator returning textures based on a single other texture. This texture is located with the
* _dom_ and _path_ parameters of a [ParameterList].
*
* @param[domain] Resource domain of generator
*/
abstract class TextureGenerator(domain: String) : ParameterBasedGenerator(domain) {
/**
* Obtain a [ResourceLocation] to a generated texture
*
* @param[iconName] the name of the [TextureAtlasSprite] (not the full location) backing the generated texture
* @param[extraParams] additional parameters of the generated texture
*/
fun generatedResource(iconName: String, vararg extraParams: Pair<String, Any>) = ResourceLocation(
domain,
textureLocation(iconName).let {
ParameterList(
mapOf("dom" to it.resourceDomain, "path" to it.resourcePath) +
extraParams.map { Pair(it.first, it.second.toString()) },
"generate"
).toString()
}
)
/**
* Get the type and location of the texture resource encoded by the given [ParameterList].
*/
fun targetResource(params: ParameterList): Pair<ResourceType, ResourceLocation>? {
val baseTexture =
if (listOf("dom", "path").all { it in params }) ResourceLocation(params["dom"]!!, params["path"]!!)
else return null
return when(params.value?.toLowerCase()) {
"generate.png" -> COLOR to baseTexture + ".png"
"generate.png.mcmeta" -> METADATA to baseTexture + ".png.mcmeta"
"generate_n.png" -> NORMAL to baseTexture + "_n.png"
"generate_s.png" -> SPECULAR to baseTexture + "_s.png"
else -> null
}
}
override fun resourceExists(params: ParameterList) =
targetResource(params)?.second?.let { resourceManager[it] != null } ?: false
override fun getInputStream(params: ParameterList): InputStream? {
val target = targetResource(params)
return when(target?.first) {
null -> null
METADATA -> resourceManager[target!!.second]?.inputStream
else -> generate(params)?.asStream
}
}
/**
* Generate image data from the parameter list.
*/
abstract fun generate(params: ParameterList): BufferedImage?
/**
* Get a texture resource when multiple sizes may exist.
*
* @param[maxSize] Maximum size to consider. This value is progressively halved when searching for smaller versions.
* @param[maskPath] Location of the texture of the given size
*
*/
fun getMultisizeTexture(maxSize: Int, maskPath: (Int)->ResourceLocation): IResource? {
var size = maxSize
val sizes = linkedListOf<Int>()
while(size > 2) { sizes.add(size); size /= 2 }
return sizes.map { resourceManager[maskPath(it)] }.filterNotNull().firstOrNull()
}
}

View File

@@ -0,0 +1,90 @@
@file:JvmName("Utils")
package mods.octarinecore.client.resource
import mods.octarinecore.PI2
import mods.octarinecore.client.render.HSB
import mods.octarinecore.tryDefault
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.client.resources.IResource
import net.minecraft.client.resources.IResourceManager
import net.minecraft.client.resources.SimpleReloadableResourceManager
import net.minecraft.util.ResourceLocation
import java.awt.image.BufferedImage
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.lang.Math.*
import javax.imageio.ImageIO
/** Concise getter for the Minecraft resource manager. */
val resourceManager: SimpleReloadableResourceManager get() = Minecraft.getMinecraft().resourceManager as SimpleReloadableResourceManager
/** Append a string to the [ResourceLocation]'s path. */
operator fun ResourceLocation.plus(str: String) = ResourceLocation(resourceDomain, resourcePath + str)
/** Index operator to get a resource. */
operator fun IResourceManager.get(domain: String, path: String): IResource? = get(ResourceLocation(domain, path))
/** Index operator to get a resource. */
operator fun IResourceManager.get(location: ResourceLocation): IResource? = tryDefault(null) { getResource(location) }
/** Load an image resource. */
fun IResource.loadImage() = ImageIO.read(this.inputStream)
/** Get the lines of a text resource. */
fun IResource.getLines(): List<String> {
val result = arrayListOf<String>()
inputStream.bufferedReader().useLines { it.forEach { result.add(it) } }
return result
}
/** Index operator to get the RGB value of a pixel. */
operator fun BufferedImage.get(x: Int, y: Int) = this.getRGB(x, y)
/** Index operator to set the RGB value of a pixel. */
operator fun BufferedImage.set(x: Int, y: Int, value: Int) = this.setRGB(x, y, value)
/** Get an [InputStream] to an image object in PNG format. */
val BufferedImage.asStream: InputStream get() =
ByteArrayInputStream(ByteArrayOutputStream().let { ImageIO.write(this, "PNG", it); it.toByteArray() })
/**
* Calculate the average color of a texture.
*
* Only non-transparent pixels are considered. Averages are taken in the HSB color space (note: Hue is a circular average),
* and the result transformed back to the RGB color space.
*/
val TextureAtlasSprite.averageColor: Int? get() {
val locationNoDirs = ResourceLocation(iconName)
val locationWithDirs = ResourceLocation(locationNoDirs.resourceDomain, "textures/blocks/%s.png".format(locationNoDirs.resourcePath))
val image = resourceManager[locationWithDirs]?.loadImage() ?: return null
var numOpaque = 0
var sumHueX = 0.0
var sumHueY = 0.0
var sumSaturation = 0.0f
var sumBrightness = 0.0f
for (x in 0..image.width - 1)
for (y in 0..image.height - 1) {
val pixel = image[x, y]
val alpha = (pixel shr 24) and 255
val hsb = HSB.fromColor(pixel)
if (alpha == 255) {
numOpaque++
sumHueX += cos((hsb.hue.toDouble() - 0.5) * PI2)
sumHueY += sin((hsb.hue.toDouble() - 0.5) * PI2)
sumSaturation += hsb.saturation
sumBrightness += hsb.brightness
}
}
// circular average - transform sum vector to polar angle
val avgHue = (atan2(sumHueY.toDouble(), sumHueX.toDouble()) / PI2 + 0.5).toFloat()
return HSB(avgHue, sumSaturation / numOpaque.toFloat(), sumBrightness / numOpaque.toFloat()).asColor
}
/**
* Get the actual location of a texture from the name of its [TextureAtlasSprite].
*/
fun textureLocation(iconName: String) = ResourceLocation(iconName).let {
ResourceLocation(it.resourceDomain, "textures/blocks/${it.resourcePath}")
}