[WIP] initial Fabric port

major package refactoring
This commit is contained in:
octarine-noise
2020-02-05 13:42:48 +01:00
parent 2252fb3b42
commit df50f61b0d
151 changed files with 5181 additions and 5663 deletions
@@ -0,0 +1,103 @@
package mods.betterfoliage.resource.discovery
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BlockModelsReloadCallback
import mods.betterfoliage.ModelLoadingCallback
import mods.betterfoliage.util.HasLogger
import mods.betterfoliage.util.Invalidator
import mods.betterfoliage.util.YarnHelper
import mods.betterfoliage.util.get
import net.fabricmc.fabric.api.event.client.ClientSpriteRegistryCallback
import net.minecraft.block.BlockState
import net.minecraft.client.MinecraftClient
import net.minecraft.client.render.block.BlockModels
import net.minecraft.client.render.model.BakedModel
import net.minecraft.client.render.model.ModelLoader
import net.minecraft.client.texture.SpriteAtlasTexture
import net.minecraft.resource.ResourceManager
import net.minecraft.util.Identifier
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer
import java.util.function.Supplier
// net.minecraft.client.render.block.BlockModels.models
val BlockModels_models = YarnHelper.requiredField<Map<BlockState, BakedModel>>("net.minecraft.class_773", "field_4162", "Ljava/util/Map;")
/** Threadsafe collector for sprite IDs */
class SpriteCollectorSync {
val idSet = Collections.synchronizedSet(mutableSetOf<Identifier>())
fun add(id: Identifier) = id.apply { idSet.add(this) }
fun dump(target: ClientSpriteRegistryCallback.Registry) { idSet.forEach { target.register(it) } }
}
class BakedModelReplacer : ModelLoadingCallback, ClientSpriteRegistryCallback, BlockModelsReloadCallback, Invalidator, HasLogger {
override val logger get() = BetterFoliage.logDetail
val discoverers = mutableListOf<ModelDiscoveryBase>()
override val callbacks = mutableListOf<WeakReference<()->Unit>>()
protected var keys = emptyMap<BlockState, BlockRenderKey>()
operator fun get(state: BlockState) = keys[state]
inline fun <reified T> getTyped(state: BlockState) = get(state) as? T
var currentLoader: ModelLoader? = null
override fun beginLoadModels(loader: ModelLoader, manager: ResourceManager) {
currentLoader = loader
log("reloading block discovery configuration")
BetterFoliage.blockConfig.reloadConfig(manager)
invalidate()
}
override fun registerSprites(atlasTexture: SpriteAtlasTexture, registry: ClientSpriteRegistryCallback.Registry) {
log("discovering blocks")
val idSet = Collections.synchronizedSet(mutableSetOf<Identifier>())
val allKeys = discoverers.map {
// run model discoverers in parallel
CompletableFuture.supplyAsync(Supplier {
it.discover(currentLoader!!, Consumer { idSet.add(it) })
}, MinecraftClient.getInstance())
}.map { it.join() }
idSet.forEach { registry.register(it) }
val result = mutableMapOf<BlockState, BlockRenderKey>()
allKeys.forEach { keys ->
keys.entries.forEach { (state, key) ->
val oldKey = result[state]
if (oldKey != null) log("Replacing $oldKey with $key for state $state")
else log("Adding replacement $key for state $state")
result[state] = key
}
}
keys = result
}
override fun reloadBlockModels(blockModels: BlockModels) {
log("block model baking finished")
val modelMap = blockModels[BlockModels_models] as MutableMap<BlockState, BakedModel>
keys.forEach { (state, key) ->
val oldModel = modelMap[state]
if (oldModel == null) log("Cannot find model for state $state, ignoring")
else {
try {
val newModel = key.replace(oldModel, state)
modelMap[state] = newModel
log("Replaced model for state $state with $key")
} catch (e: Exception) {
log("Error creating model for state $state with $key", e)
}
}
}
}
init {
ModelLoadingCallback.EVENT.register(this)
ClientSpriteRegistryCallback.event(SpriteAtlasTexture.BLOCK_ATLAS_TEX).register(this)
BlockModelsReloadCallback.EVENT.register(this)
}
}
@@ -0,0 +1,53 @@
package mods.betterfoliage.resource.discovery
import com.google.common.base.Joiner
import mods.betterfoliage.util.YarnHelper
import mods.betterfoliage.util.get
import mods.betterfoliage.util.stripStart
import net.minecraft.block.BlockState
import net.minecraft.client.render.model.json.JsonUnbakedModel
import net.minecraft.util.Identifier
import java.util.function.Consumer
// net.minecraft.client.render.model.json.JsonUnbakedModel.parent
val JsonUnbakedModel_parent = YarnHelper.requiredField<JsonUnbakedModel>("net.minecraft.class_793", "field_4253", "Lnet/minecraft/class_793;")
// net.minecraft.client.render.model.json.JsonUnbakedModel.parentId
val JsonUnbakedModel_parentId = YarnHelper.requiredField<Identifier>("net.minecraft.class_793", "field_4247", "Lnet/minecraft/class_2960;")
fun Pair<JsonUnbakedModel, Identifier>.derivesFrom(targetLocation: Identifier): Boolean {
if (second.stripStart("models/") == targetLocation) return true
if (first[JsonUnbakedModel_parent] != null && first[JsonUnbakedModel_parentId] != null)
return Pair(first[JsonUnbakedModel_parent]!!, first[JsonUnbakedModel_parentId]!!).derivesFrom(targetLocation)
return false
}
abstract class ConfigurableModelDiscovery : ModelDiscoveryBase() {
abstract val matchClasses: IBlockMatcher
abstract val modelTextures: List<ModelTextureList>
abstract fun processModel(state: BlockState, textures: List<String>, atlas: Consumer<Identifier>): BlockRenderKey?
override fun processModel(ctx: ModelDiscoveryContext, atlas: Consumer<Identifier>): BlockRenderKey? {
val matchClass = matchClasses.matchingClass(ctx.state.block) ?: return null
log("block state ${ctx.state.toString()}")
log(" class ${ctx.state.block.javaClass.name} matches ${matchClass.name}")
(ctx.models.filter { it.first is JsonUnbakedModel } as List<Pair<JsonUnbakedModel, Identifier>>).forEach { (model, location) ->
val modelMatch = modelTextures.firstOrNull { (model to location).derivesFrom(it.modelLocation) }
if (modelMatch != null) {
log(" model ${model} matches ${modelMatch.modelLocation}")
val textures = modelMatch.textureNames.map { it to model.resolveTexture(it) }
val texMapString = Joiner.on(", ").join(textures.map { "${it.first}=${it.second}" })
log(" sprites [$texMapString]")
if (textures.all { it.second != "missingno" }) {
// found a valid model (all required textures exist)
return processModel(ctx.state, textures.map { it.second }, atlas)
}
}
}
return null
}
}
@@ -0,0 +1,77 @@
package mods.betterfoliage.resource.discovery
import mods.betterfoliage.util.getLines
import mods.betterfoliage.util.INTERMEDIARY
import mods.betterfoliage.util.getJavaClass
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.block.Block
import net.minecraft.resource.ResourceManager
import net.minecraft.util.Identifier
import org.apache.logging.log4j.Logger
interface IBlockMatcher {
fun matchesClass(block: Block): Boolean
fun matchingClass(block: Block): Class<*>?
}
class SimpleBlockMatcher(vararg val classes: Class<*>) : IBlockMatcher {
override fun matchesClass(block: Block) = matchingClass(block) != null
override fun matchingClass(block: Block): Class<*>? {
val blockClass = block.javaClass
classes.forEach { if (it.isAssignableFrom(blockClass)) return it }
return null
}
}
class ConfigurableBlockMatcher(val logger: Logger, val location: Identifier) : IBlockMatcher {
val blackList = mutableListOf<Class<*>>()
val whiteList = mutableListOf<Class<*>>()
override fun matchesClass(block: Block): Boolean {
val blockClass = block.javaClass
blackList.forEach { if (it.isAssignableFrom(blockClass)) return false }
whiteList.forEach { if (it.isAssignableFrom(blockClass)) return true }
return false
}
override fun matchingClass(block: Block): Class<*>? {
val blockClass = block.javaClass
blackList.forEach { if (it.isAssignableFrom(blockClass)) return null }
whiteList.forEach { if (it.isAssignableFrom(blockClass)) return it }
return null
}
fun readDefaults(manager: ResourceManager) {
blackList.clear()
whiteList.clear()
manager.getAllResources(location).forEach { resource ->
logger.debug("Reading resource $location from pack ${resource.resourcePackName}")
resource.getLines().map{ it.trim() }.filter { !it.startsWith("//") && it.isNotEmpty() }.forEach { line ->
if (line.startsWith("-")) getBlockClass(line.substring(1))?.let { blackList.add(it) }
else getBlockClass(line)?.let { whiteList.add(it) }
}
}
}
fun getBlockClass(name: String) = getJavaClass(FabricLoader.getInstance().mappingResolver.mapClassName(INTERMEDIARY, name))
}
data class ModelTextureList(val modelLocation: Identifier, val textureNames: List<String>) {
constructor(vararg args: String) : this(Identifier(args[0]), listOf(*args).drop(1))
}
class ModelTextureListConfiguration(val logger: Logger, val location: Identifier) {
val modelList = mutableListOf<ModelTextureList>()
fun readDefaults(manager: ResourceManager) {
manager.getAllResources(location).forEach { resource ->
logger.debug("Reading resource $location from pack ${resource.resourcePackName}")
resource.getLines().map{ it.trim() }.filter { !it.startsWith("//") && it.isNotEmpty() }.forEach { line ->
val elements = line.split(",")
modelList.add(ModelTextureList(Identifier(elements.first()), elements.drop(1)))
}
}
}
}
@@ -0,0 +1,76 @@
package mods.betterfoliage.resource.discovery
import mods.betterfoliage.util.HasLogger
import net.minecraft.block.BlockState
import net.minecraft.client.render.block.BlockModels
import net.minecraft.client.render.model.BakedModel
import net.minecraft.client.render.model.ModelLoader
import net.minecraft.client.render.model.UnbakedModel
import net.minecraft.client.render.model.json.JsonUnbakedModel
import net.minecraft.client.render.model.json.ModelVariant
import net.minecraft.client.render.model.json.WeightedUnbakedModel
import net.minecraft.client.texture.SpriteAtlasTexture
import net.minecraft.client.util.ModelIdentifier
import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry
import java.util.function.Consumer
typealias RenderKeyFactory = (SpriteAtlasTexture)->BlockRenderKey
interface BlockRenderKey {
fun replace(model: BakedModel, state: BlockState): BakedModel = model
}
fun ModelLoader.iterateModels(func: (ModelDiscoveryContext)->Unit) {
Registry.BLOCK.flatMap { block ->
block.stateFactory.states.map { state -> state to BlockModels.getModelId(state) }
}.forEach { (state, stateModelResource) ->
func(ModelDiscoveryContext(this, state, stateModelResource))
}
}
/**
* Information about a single [BlockState] and all the [UnbakedModel]s it could render as.
*/
class ModelDiscoveryContext(
loader: ModelLoader,
val state: BlockState,
val modelId: ModelIdentifier
) {
val models = loader.unwrapVariants(loader.getOrLoadModel(modelId) to modelId)
.filter { it.second != loader.getOrLoadModel(ModelLoader.MISSING) }
fun ModelLoader.unwrapVariants(modelAndLoc: Pair<UnbakedModel, Identifier>): List<Pair<UnbakedModel, Identifier>> = when(val model = modelAndLoc.first) {
is WeightedUnbakedModel -> (model.variants as List<ModelVariant>).flatMap {
variant -> unwrapVariants(getOrLoadModel(variant.location) to variant.location)
}
is JsonUnbakedModel -> listOf(modelAndLoc)
else -> emptyList()
}
}
interface ModelDiscovery {
fun discover(loader: ModelLoader, atlas: Consumer<Identifier>): Map<BlockState, BlockRenderKey>
}
abstract class ModelDiscoveryBase : ModelDiscovery, HasLogger {
override fun discover(loader: ModelLoader, atlas: Consumer<Identifier>): Map<BlockState, BlockRenderKey> {
val keys = mutableMapOf<BlockState, BlockRenderKey>()
var errors = 0
loader.iterateModels { ctx ->
try {
val result = processModel(ctx, atlas)
result?.let { keys[ctx.state] = it }
} catch (e: Exception) {
errors++
}
}
log("${keys.size} BlockStates discovered, $errors errors")
return keys
}
abstract fun processModel(ctx: ModelDiscoveryContext, atlas: Consumer<Identifier>): BlockRenderKey?
}
@@ -0,0 +1,39 @@
package mods.betterfoliage.resource.generated
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.bytes
import mods.betterfoliage.util.loadSprite
import net.minecraft.resource.ResourceManager
import net.minecraft.util.Identifier
import java.awt.image.BufferedImage
import java.lang.Math.max
data class CenteredSprite(val sprite: Identifier, val atlas: Atlas = Atlas.BLOCKS, val aspectHeight: Int = 1, val aspectWidth: Int = 1) {
fun register(pack: GeneratedBlockTexturePack) = pack.register(this, this::draw)
fun draw(resourceManager: ResourceManager): ByteArray {
val baseTexture = resourceManager.loadSprite(atlas.wrap(sprite))
val frameWidth = baseTexture.width
val frameHeight = baseTexture.width * aspectHeight / aspectWidth
val frames = baseTexture.height / frameHeight
val size = max(frameWidth, frameHeight)
val result = BufferedImage(size, size * frames, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = result.createGraphics()
// iterate all frames
for (frame in 0 until frames) {
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 result.bytes
}
}
@@ -0,0 +1,92 @@
package mods.betterfoliage.resource.generated
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.HasLogger
import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener
import net.minecraft.client.resource.ClientResourcePackContainer
import net.minecraft.resource.*
import net.minecraft.resource.ResourcePackContainer.InsertionPosition
import net.minecraft.resource.ResourceType.CLIENT_RESOURCES
import net.minecraft.resource.metadata.ResourceMetadataReader
import net.minecraft.text.LiteralText
import net.minecraft.util.Identifier
import net.minecraft.util.profiler.Profiler
import org.apache.logging.log4j.Logger
import java.io.IOException
import java.lang.IllegalStateException
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executor
import java.util.function.Predicate
import java.util.function.Supplier
/**
* [ResourcePack] containing generated block textures
*
* @param[reloadId] Fabric ID of the pack
* @param[nameSpace] Resource namespace of pack
* @param[packName] Friendly name of pack
* @param[packDesc] Description of pack
* @param[logger] Logger to log to when generating resources
*/
class GeneratedBlockTexturePack(val reloadId: Identifier, val nameSpace: String, val packName: String, val packDesc: String, override val logger: Logger) : HasLogger, ResourcePack, IdentifiableResourceReloadListener {
override fun getName() = reloadId.toString()
override fun getNamespaces(type: ResourceType) = setOf(nameSpace)
override fun <T : Any?> parseMetadata(deserializer: ResourceMetadataReader<T>) = null
override fun openRoot(id: String) = null
override fun findResources(type: ResourceType, path: String, maxDepth: Int, filter: Predicate<String>) = emptyList<Identifier>()
override fun close() {}
protected var manager: ResourceManager? = null
val identifiers: MutableMap<Any, Identifier> = Collections.synchronizedMap(mutableMapOf<Any, Identifier>())
val resources: MutableMap<Identifier, ByteArray> = Collections.synchronizedMap(mutableMapOf<Identifier, ByteArray>())
fun register(key: Any, func: (ResourceManager)->ByteArray): Identifier {
if (manager == null) throw IllegalStateException("Cannot register resources unless resource manager is being reloaded")
identifiers[key]?.let { return it }
val id = Identifier(nameSpace, UUID.randomUUID().toString())
val resource = func(manager!!)
identifiers[key] = id
resources[Atlas.BLOCKS.wrap(id)] = resource
log("generated resource $key -> $id")
return id
}
override fun open(type: ResourceType, id: Identifier) =
if (type != CLIENT_RESOURCES) null else
try { resources[id]!!.inputStream() }
catch (e: ExecutionException) { (e.cause as? IOException)?.let { throw it } } // rethrow wrapped IOException if present
override fun contains(type: ResourceType, id: Identifier) =
type == CLIENT_RESOURCES && resources.containsKey(id)
override fun reload(synchronizer: ResourceReloadListener.Synchronizer, manager: ResourceManager, prepareProfiler: Profiler, applyProfiler: Profiler, prepareExecutor: Executor, applyExecutor: Executor): CompletableFuture<Void> {
this.manager = manager
return synchronizer.whenPrepared(null).thenRun {
this.manager = null
identifiers.clear()
resources.clear()
}
}
override fun getFabricId() = reloadId
/**
* Supplier for this resource pack. Adds pack as always-on and hidden.
*/
val finder = object : ResourcePackCreator {
val packInfo = ClientResourcePackContainer(
packName, true, Supplier { this@GeneratedBlockTexturePack },
LiteralText(packName),
LiteralText(packDesc),
ResourcePackCompatibility.COMPATIBLE, InsertionPosition.TOP, true, null
)
override fun <T : ResourcePackContainer> registerContainer(nameToPackMap: MutableMap<String, T>, packInfoFactory: ResourcePackContainer.Factory<T>) {
(nameToPackMap as MutableMap<String, ResourcePackContainer>)[reloadId.toString()] = packInfo
}
}
}
@@ -0,0 +1,50 @@
package mods.betterfoliage.resource.generated
import mods.betterfoliage.util.*
import net.minecraft.resource.ResourceManager
import net.minecraft.util.Identifier
import java.awt.image.BufferedImage
/**
* Generate Short Grass textures from [Blocks.tallgrass] block textures.
* The bottom 3/8 of the base texture is chopped off.
*
* @param[domain] Resource domain of generator
*/
data class GeneratedGrassSprite(val sprite: Identifier, val isSnowed: Boolean, val atlas: Atlas = Atlas.BLOCKS) {
constructor(sprite: String, isSnowed: Boolean) : this(Identifier(sprite), isSnowed)
fun register(pack: GeneratedBlockTexturePack) = pack.register(this, this::draw)
fun draw(resourceManager: ResourceManager): ByteArray {
val baseTexture = resourceManager.loadSprite(atlas.wrap(sprite))
val result = BufferedImage(baseTexture.width, baseTexture.height, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = result.createGraphics()
val size = baseTexture.width
val frames = baseTexture.height / size
// iterate all frames
for (frame in 0 until frames) {
val baseFrame = baseTexture.getSubimage(0, size * frame, size, size)
val grassFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR)
// draw bottom half of texture
grassFrame.createGraphics().apply {
drawImage(baseFrame, 0, 3 * size / 8, null)
}
// add to animated png
graphics.drawImage(grassFrame, 0, size * frame, null)
}
// blend with white if snowed
if (isSnowed) {
for (x in 0..result.width - 1) for (y in 0..result.height - 1) {
result[x, y] = blendRGB(result[x, y], 16777215, 2, 3)
}
}
return result.bytes
}
}
@@ -0,0 +1,86 @@
package mods.betterfoliage.resource.generated
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.util.*
import net.minecraft.resource.Resource
import net.minecraft.resource.ResourceManager
import net.minecraft.util.Identifier
import java.awt.image.BufferedImage
/**
* Generate round leaf textures from leaf block textures.
* The base texture is tiled 2x2, then parts of it are made transparent by applying a mask to the alpha channel.
*
* Different leaf types may have their own alpha mask.
*
* @param[domain] Resource domain of generator
*/
data class GeneratedLeafSprite(val sprite: Identifier, val leafType: String, val atlas: Atlas = Atlas.BLOCKS) {
fun register(pack: GeneratedBlockTexturePack) = pack.register(this, this::draw)
fun draw(resourceManager: ResourceManager): ByteArray {
val baseTexture = resourceManager.loadSprite(atlas.wrap(sprite))
val size = baseTexture.width
val frames = baseTexture.height / size
val maskTexture = (getLeafMask(leafType, size * 2) ?: getLeafMask("default", size * 2))?.loadImage()
fun scale(i: Int) = i * maskTexture!!.width / (size * 2)
val result = BufferedImage(size * 2, size * 2 * frames, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = result.createGraphics()
// iterate all frames
for (frame in 0 until frames) {
val baseFrame = baseTexture.getSubimage(0, size * frame, size, size)
val leafFrame = BufferedImage(size * 2, size * 2, BufferedImage.TYPE_4BYTE_ABGR)
// tile leaf texture 2x2
leafFrame.createGraphics().apply {
drawImage(baseFrame, 0, 0, null)
drawImage(baseFrame, 0, size, null)
drawImage(baseFrame, size, 0, null)
drawImage(baseFrame, size, size, null)
}
// overlay alpha mask
if (maskTexture != null) {
for (x in 0 until size * 2) for (y in 0 until size * 2) {
val basePixel = leafFrame[x, y].toLong() and 0xFFFFFFFFL
val maskPixel = maskTexture[scale(x), scale(y)].toLong() and 0xFF000000L or 0xFFFFFFL
leafFrame[x, y] = (basePixel and maskPixel).toInt()
}
}
// add to animated png
graphics.drawImage(leafFrame, 0, size * frame * 2, null)
}
return result.bytes
}
/**
* Get the alpha mask to use
*
* @param[type] Alpha mask type.
* @param[maxSize] Preferred mask size.
*/
fun getLeafMask(type: String, maxSize: Int) = getMultisizeTexture(maxSize) { size ->
Atlas.BLOCKS.wrap(Identifier(BetterFoliage.MOD_ID, "blocks/leafmask_${size}_${type}"))
}
/**
* 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)->Identifier): Resource? {
var size = maxSize
val sizes = mutableListOf<Int>()
while(size > 2) { sizes.add(size); size /= 2 }
return sizes.findFirst { resourceManager[maskPath(it)] }
}
}
@@ -0,0 +1,114 @@
package mods.betterfoliage.resource.model
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.allDirections
import mods.betterfoliage.util.findFirst
import net.minecraft.block.BlockRenderLayer
import net.minecraft.block.BlockState
import net.minecraft.client.render.VertexFormat
import net.minecraft.client.render.VertexFormatElement
import net.minecraft.client.render.VertexFormatElement.Format.FLOAT
import net.minecraft.client.render.VertexFormatElement.Format.UBYTE
import net.minecraft.client.render.VertexFormatElement.Type.*
import net.minecraft.client.render.VertexFormatElement.Type.UV
import net.minecraft.client.render.VertexFormats
import net.minecraft.client.render.model.BakedModel
import net.minecraft.client.render.model.BakedQuad
import net.minecraft.util.math.Direction
import java.lang.Float
import java.util.*
interface BakedModelConverter {
/**
* Convert baked model. Returns null if conversion unsuccessful (wrong input type).
* @param model Input model
* @param converter Converter to use for converting nested models.
*/
fun convert(model: BakedModel, converter: BakedModelConverter): BakedModel?
companion object {
fun of(func: (BakedModel, BakedModelConverter)->BakedModel?) = object : BakedModelConverter {
override fun convert(model: BakedModel, converter: BakedModelConverter) = func(model, converter)
}
val identity = of { model, _ -> model }
}
}
/**
* Convert [BakedModel] using the provided list of [BakedModelConverter]s (in order).
* If all converters fail, gives the original model back.
*/
fun List<BakedModelConverter>.convert(model: BakedModel) = object : BakedModelConverter {
val converters = this@convert + BakedModelConverter.identity
override fun convert(model: BakedModel, converter: BakedModelConverter) = converters.findFirst { it.convert(model, converter) }
}.let { converterStack ->
// we are guaranteed a result here because of the identity converter
converterStack.convert(model, converterStack)!!
}
/** List of converters without meaningful configuration that should always be used */
val COMMON_MESH_CONVERTERS = listOf(WrappedWeightedModel.converter)
/**
* Convert [BakedModel] into one using fabric-rendering-api [Mesh] instead of the vanilla pipeline.
* @param renderLayerOverride Use the given [BlockRenderLayer] for the [Mesh]
* instead of the one declared by the corresponding [Block]
*/
fun meshifyStandard(model: BakedModel, state: BlockState, renderLayerOverride: BlockRenderLayer? = null) =
(COMMON_MESH_CONVERTERS + WrappedMeshModel.converter(state, renderLayerOverride = renderLayerOverride)).convert(model)
/**
* Convert a vanilla [BakedModel] into intermediate [Quad]s
* Vertex normals not supported (yet)
* Vertex data elements not aligned to 32 bit boundaries not supported
*/
fun unbakeQuads(model: BakedModel, state: BlockState, random: Random, unshade: Boolean): List<Quad> {
return (allDirections.toList() + null as Direction?).flatMap { face ->
model.getQuads(state, face, random).mapIndexed { qIdx, bakedQuad ->
var quad = Quad(Vertex(), Vertex(), Vertex(), Vertex(), face = face, colorIndex = bakedQuad.colorIndex, sprite = bakedQuad.sprite)
val format = quadVertexFormat(bakedQuad)
val stride = format.vertexSizeInteger
format.getIntOffset(POSITION, FLOAT, 3)?.let { posOffset ->
quad = quad.transformVI { vertex, vIdx -> vertex.copy(xyz = Double3(
x = Float.intBitsToFloat(bakedQuad.vertexData[vIdx * stride + posOffset + 0]).toDouble(),
y = Float.intBitsToFloat(bakedQuad.vertexData[vIdx * stride + posOffset + 1]).toDouble(),
z = Float.intBitsToFloat(bakedQuad.vertexData[vIdx * stride + posOffset + 2]).toDouble()
)) }
}
format.getIntOffset(COLOR, UBYTE, 4)?.let { colorOffset ->
quad = quad.transformVI { vertex, vIdx -> vertex.copy(
color = Color(bakedQuad.vertexData[vIdx * stride + colorOffset])
) }
}
format.getIntOffset(UV, FLOAT, 2, 0)?.let { uvOffset ->
quad = quad.transformVI { vertex, vIdx -> vertex.copy(uv = UV(
u = Float.intBitsToFloat(bakedQuad.vertexData[vIdx * stride + uvOffset + 0]).toDouble(),
v = Float.intBitsToFloat(bakedQuad.vertexData[vIdx * stride + uvOffset + 1]).toDouble()
)) }
}
quad = quad.transformV { it.copy(uv = it.uv.unbake(quad.sprite!!)) }.move(Double3(-0.5, -0.5, -0.5))
if (unshade) quad = quad.transformV { it.copy(color = it.color * (1.0f / Color.bakeShade(quad.face))) }
quad
}
}
}
/** Get the byte offset of the [VertexFormatElement] matching the given criteria */
fun VertexFormat.getByteOffset(type: VertexFormatElement.Type, format: VertexFormatElement.Format, count: Int, index: Int = 0): Int? {
elements.forEachIndexed { idx, element ->
if (element.type == type && element.format == format && element.count == count && element.index == index)
return getElementOffset(idx)
}
return null
}
/**
* Get the int (32 bit) offset of the [VertexFormatElement] matching the given criteria
* Returns null if the element is not properly aligned
*/
fun VertexFormat.getIntOffset(type: VertexFormatElement.Type, format: VertexFormatElement.Format, count: Int, index: Int = 0) =
getByteOffset(type, format, count, index)?.let { if (it % 4 == 0) it / 4 else null }
/** Function to determine [VertexFormat] used by [BakedQuad] */
var quadVertexFormat: (BakedQuad)->VertexFormat = { VertexFormats.POSITION_COLOR_UV_LMAP }
@@ -0,0 +1,230 @@
package mods.betterfoliage.resource.model
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.*
import mods.betterfoliage.util.minmax
import net.fabricmc.fabric.api.renderer.v1.RendererAccess
import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh
import net.minecraft.block.BlockRenderLayer
import net.minecraft.client.texture.MissingSprite
import net.minecraft.client.texture.Sprite
import net.minecraft.util.math.Direction
import net.minecraft.util.math.Direction.*
import java.lang.Math.max
import java.lang.Math.min
import kotlin.math.cos
import kotlin.math.sin
import kotlin.random.Random
/**
* Vertex UV coordinates
*
* Zero-centered: sprite coordinates fall between (-0.5, 0.5)
*/
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)
fun unbake(sprite: Sprite) = UV(
(u - sprite.minU.toDouble()) / (sprite.maxU - sprite.minU).toDouble() - 0.5,
(v - sprite.minV.toDouble()) / (sprite.maxV - sprite.minV).toDouble() - 0.5
)
}
data class Color(val alpha: Int, val red: Int, val green: Int, val blue: Int) {
constructor(combined: Int) : this(combined shr 24 and 255, combined shr 16 and 255, combined shr 8 and 255, combined and 255)
val asInt get() = (alpha shl 24) or (red shl 16) or (green shl 8) or blue
operator fun times(f: Float) = Color(
alpha,
(f * red.toFloat()).toInt().coerceIn(0 until 256),
(f * green.toFloat()).toInt().coerceIn(0 until 256),
(f * blue.toFloat()).toInt().coerceIn(0 until 256)
)
companion object {
val white get() = Color(255, 255, 255, 255)
/** Amount of vanilla diffuse lighting applied to face quads */
fun bakeShade(dir: Direction?) = when(dir) {
DOWN -> 0.5f
NORTH, SOUTH -> 0.8f
EAST, WEST -> 0.6f
else -> 1.0f
}
}
}
data class HSB(var hue: Float, var saturation: Float, var brightness: Float) {
companion object {
fun fromColor(color: Int): HSB {
val hsbVals = java.awt.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() = java.awt.Color.HSBtoRGB(hue, saturation, brightness)
}
/**
* Model vertex
*
* @param[xyz] x, y, z coordinates
* @param[uv] u, v coordinates
* @param[color] vertex color RGB components
* @param[alpha] vertex color alpha component
*/
data class Vertex(val xyz: Double3 = Double3(0.0, 0.0, 0.0),
val uv: UV = UV(0.0, 0.0),
val color: Color = Color.white,
val alpha: Int = 255,
val normal: Double3? = null
)
/**
* Intermediate (fabric-renderer-api independent) representation of model quad
* Immutable, double-precision
* Zero-centered (both XYZ and UV) coordinates for simpler rotation/mirroring
*/
data class Quad(
val v1: Vertex, val v2: Vertex, val v3: Vertex, val v4: Vertex,
val sprite: Sprite? = null,
val colorIndex: Int = -1,
val face: Direction? = null
) {
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 = copy(
v1 = trans(v1, 0), v2 = trans(v2, 1), v3 = trans(v3, 2), v4 = 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, Direction>) = 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 rotate(rot: Rotation) = transformV { it.copy(xyz = it.xyz.rotate(rot), normal = it.normal?.rotate(rot)) }.copy(face = face?.rotate(rot))
fun rotateZ(angle: Double) = transformV { it.copy(
xyz = Double3(it.xyz.x * cos(angle) + it.xyz.z * sin(angle), it.xyz.y, it.xyz.z * cos(angle) - it.xyz.x * sin(angle)),
normal = it.normal?.let { normal-> Double3(normal.x * cos(angle) + normal.z * sin(angle), normal.y, normal.z * cos(angle) - normal.x * sin(angle)) }
) }
fun scaleUV (scale: Double) = transformV { it.copy(uv = UV(it.uv.u * scale, it.uv.v * scale)) }
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 scrambleUV(random: Random, canFlipU: Boolean, canFlipV: Boolean, canRotate: Boolean) = this
.mirrorUV(canFlipU && random.nextBoolean(), canFlipV && random.nextBoolean())
.let { if (canRotate) it.rotateUV(random.nextInt(4)) else it }
fun sprite(sprite: Sprite) = copy(sprite = sprite)
fun color(color: Color) = transformV { it.copy(color = color) }
fun color(color: Int) = transformV { it.copy(color = Color(color)) }
fun colorIndex(colorIndex: Int) = copy(colorIndex = colorIndex)
fun colorAndIndex(color: Int?) = color(color ?: Color.white.asInt).colorIndex(if (color == null) 0 else -1)
val flipped: Quad get() = Quad(v4, v3, v2, v1, sprite, colorIndex)
fun cycleVertices(n: Int) = when(n % 4) {
1 -> Quad(v2, v3, v4, v1)
2 -> Quad(v3, v4, v1, v2)
3 -> Quad(v4, v1, v2, v3)
else -> this.copy()
}
companion object {
fun mix(first: Quad, second: Quad, vertexFactory: (Vertex, Vertex)-> Vertex) = Quad(
v1 = vertexFactory(first.v1, second.v1),
v2 = vertexFactory(first.v2, second.v2),
v3 = vertexFactory(first.v3, second.v3),
v4 = vertexFactory(first.v4, second.v4)
)
}
}
fun List<Quad>.transform(trans: Quad.()-> Quad) = map { it.trans() }
fun Array<List<Quad>>.transform(trans: Quad.(Int)-> Quad) = mapIndexed { idx, qList -> qList.map { it.trans(idx) } }.toTypedArray()
fun List<Quad>.withOpposites() = flatMap { listOf(it, it.flipped) }
fun Array<List<Quad>>.withOpposites() = map { it.withOpposites() }.toTypedArray()
/**
* Pour quad data into a fabric-renderer-api Mesh
*/
fun List<Quad>.build(layer: BlockRenderLayer, noDiffuse: Boolean = false, flatLighting: Boolean = false): Mesh {
val renderer = RendererAccess.INSTANCE.renderer
val material = renderer.materialFinder().blendMode(0, layer).disableAo(0, flatLighting).disableDiffuse(0, noDiffuse).find()
val builder = renderer.meshBuilder()
builder.emitter.apply {
forEach { quad ->
val sprite = quad.sprite ?: Atlas.BLOCKS.atlas[MissingSprite.getMissingSpriteId()]!!
quad.verts.forEachIndexed { idx, vertex ->
pos(idx, (vertex.xyz + Double3(0.5, 0.5, 0.5)).asVec3f)
sprite(idx, 0,
(sprite.maxU - sprite.minU) * (vertex.uv.u.toFloat() + 0.5f) + sprite.minU,
(sprite.maxV - sprite.minV) * (vertex.uv.v.toFloat() + 0.5f) + sprite.minV
)
spriteColor(idx, 0, vertex.color.asInt)
}
cullFace(quad.face)
colorIndex(quad.colorIndex)
material(material)
emit()
}
}
return builder.build()
}
fun Array<List<Quad>>.build(layer: BlockRenderLayer, noDiffuse: Boolean = false, flatLighting: Boolean = false) = map { it.build(layer, noDiffuse, flatLighting) }.toTypedArray()
/**
* 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).
*/
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: Direction): Quad {
val base = face.vec * 0.5
val top = boxFaces[face].top * 0.5
val left = boxFaces[face].left * 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),
face = face
)
}
fun xzDisk(modelIdx: Int) = (PI2 * modelIdx / 64.0).let { Double3(cos(it), 0.0, sin(it)) }
@@ -0,0 +1,70 @@
package mods.betterfoliage.resource.model
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.get
import net.fabricmc.fabric.api.event.client.ClientSpriteRegistryCallback
import net.minecraft.client.MinecraftClient
import net.minecraft.client.texture.MissingSprite
import net.minecraft.client.texture.Sprite
import net.minecraft.client.texture.SpriteAtlasTexture
import net.minecraft.util.Identifier
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
interface SpriteSet {
val num: Int
operator fun get(idx: Int): Sprite
}
class FixedSpriteSet(val sprites: List<Sprite>) : SpriteSet {
override val num = sprites.size
override fun get(idx: Int) = sprites[idx % num]
constructor(atlas: Atlas, ids: List<Identifier>) : this(
ids.mapNotNull { atlas.atlas[it] }.let { sprites ->
if (sprites.isNotEmpty()) sprites else listOf(atlas.atlas[MissingSprite.getMissingSpriteId()]!!)
}
)
}
class SpriteDelegate(val atlas: Atlas, val idFunc: ()->Identifier) : ReadOnlyProperty<Any, Sprite>, ClientSpriteRegistryCallback {
private var id: Identifier? = null
private var value: Sprite? = null
init { ClientSpriteRegistryCallback.event(atlas.resourceId).register(this) }
override fun registerSprites(atlasTexture: SpriteAtlasTexture, registry: ClientSpriteRegistryCallback.Registry) {
id = idFunc(); value = null
registry.register(id)
}
override fun getValue(thisRef: Any, property: KProperty<*>): Sprite {
value?.let { return it }
synchronized(this) {
value?.let { return it }
atlas.atlas[id!!]!!.let { value = it; return it }
}
}
}
class SpriteSetDelegate(val atlas: Atlas, val idRegister: (Identifier)->Identifier = { it }, val idFunc: (Int)->Identifier) : ReadOnlyProperty<Any, SpriteSet>, ClientSpriteRegistryCallback {
private var idList: List<Identifier> = emptyList()
private var spriteSet: SpriteSet? = null
init { ClientSpriteRegistryCallback.event(atlas.resourceId).register(this) }
override fun registerSprites(atlasTexture: SpriteAtlasTexture, registry: ClientSpriteRegistryCallback.Registry) {
spriteSet = null
val manager = MinecraftClient.getInstance().resourceManager
idList = (0 until 16).map(idFunc).filter { manager.containsResource(atlas.wrap(it)) }.map(idRegister)
idList.forEach { registry.register(it) }
}
override fun getValue(thisRef: Any, property: KProperty<*>): SpriteSet {
spriteSet?.let { return it }
synchronized(this) {
spriteSet?.let { return it }
spriteSet = FixedSpriteSet(atlas, idList)
return spriteSet!!
}
}
}
@@ -0,0 +1,74 @@
package mods.betterfoliage.resource.model
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.*
import net.fabricmc.fabric.api.renderer.v1.RendererAccess
import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh
import net.minecraft.block.BlockRenderLayer
import net.minecraft.block.BlockRenderLayer.CUTOUT_MIPPED
import net.minecraft.client.texture.Sprite
import net.minecraft.util.Identifier
import net.minecraft.util.math.Direction.UP
data class TuftShapeKey(
val size: Double,
val height: Double,
val offset: Double3,
val flipU1: Boolean,
val flipU2: Boolean
)
fun tuftShapeSet(size: Double, heightMin: Double, heightMax: Double, hOffset: Double): Array<TuftShapeKey> {
return Array(64) { idx ->
TuftShapeKey(
size,
randomD(heightMin, heightMax),
xzDisk(idx) * randomD(hOffset / 2.0, hOffset),
randomB(),
randomB()
)
}
}
fun tuftQuadSingle(size: Double, height: Double, flipU: Boolean) =
verticalRectangle(x1 = -0.5 * size, z1 = 0.5 * size, x2 = 0.5 * size, z2 = -0.5 * size, yBottom = 0.5, yTop = 0.5 + height)
.mirrorUV(flipU, false)
fun tuftModelSet(shapes: Array<TuftShapeKey>, overrideColor: Int?, spriteGetter: (Int)->Sprite) = shapes.mapIndexed { idx, shape ->
listOf(
tuftQuadSingle(shape.size, shape.height, shape.flipU1),
tuftQuadSingle(shape.size, shape.height, shape.flipU2).rotate(rot(UP))
).map { it.move(shape.offset) }
.map { it.colorAndIndex(overrideColor) }
.map { it.sprite(spriteGetter(idx)) }
}.toTypedArray()
fun fullCubeTextured(spriteId: Identifier, overrideColor: Int?, scrambleUV: Boolean = true): Mesh {
val sprite = Atlas.BLOCKS.atlas[spriteId]!!
return allDirections.map { faceQuad(it) }
.map { if (!scrambleUV) it else it.rotateUV(randomI(max = 4)) }
.map { it.sprite(sprite) }
.map { it.colorAndIndex(overrideColor) }
.build(BlockRenderLayer.SOLID)
}
fun crossModelsRaw(num: Int, size: Double, hOffset: Double, vOffset: Double): Array<List<Quad>> {
return Array(num) { idx ->
listOf(
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41),
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41)
.rotate(rot(UP))
).map { it.scale(size) }
.map { it.move(xzDisk(idx) * hOffset) }
.map { it.move(UP.vec * randomD(-1.0, 1.0) * vOffset) }
}
}
fun crossModelsTextured(leafBase: Array<List<Quad>>, overrideColor: Int?, scrambleUV: Boolean, spriteGetter: (Int)->Sprite) = leafBase.map { leaf ->
leaf.map { if (scrambleUV) it.scrambleUV(random, canFlipU = true, canFlipV = true, canRotate = true) else it }
.map { it.colorAndIndex(overrideColor) }
.mapIndexed { idx, quad -> quad.sprite(spriteGetter(idx)) }
.withOpposites().build(CUTOUT_MIPPED)
}.toTypedArray()
fun Array<List<Quad>>.buildTufts() = withOpposites().build(CUTOUT_MIPPED)
@@ -0,0 +1,86 @@
package mods.betterfoliage.resource.model
import mods.betterfoliage.util.YarnHelper
import mods.betterfoliage.util.get
import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh
import net.fabricmc.fabric.api.renderer.v1.model.FabricBakedModel
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext
import net.minecraft.block.BlockRenderLayer
import net.minecraft.block.BlockState
import net.minecraft.client.render.model.BakedModel
import net.minecraft.client.render.model.BasicBakedModel
import net.minecraft.client.render.model.WeightedBakedModel
import net.minecraft.item.ItemStack
import net.minecraft.util.WeightedPicker
import net.minecraft.util.math.BlockPos
import net.minecraft.world.ExtendedBlockView
import java.util.*
import java.util.function.Supplier
// net.minecraft.client.render.model.WeightedBakedModel.totalWeight
val WeightedBakedModel_totalWeight = YarnHelper.requiredField<Int>("net.minecraft.class_1097", "field_5433", "I")
// net.minecraft.client.render.model.WeightedBakedModel.models
val WeightedBakedModel_models = YarnHelper.requiredField<List<WeightedPicker.Entry>>("net.minecraft.class_1097", "field_5434", "Ljava/util/List;")
// net.minecraft.client.render.model.WeightedBakedModel.ModelEntry.model
val WeightedBakedModelEntry_model = YarnHelper.requiredField<BakedModel>("net.minecraft.class_1097\$class_1099", "field_5437", "Lnet/minecraft/class_1087;")
// net.minecraft.util.WeightedPicker.Entry.weight
val WeightedPickerEntry_weight = YarnHelper.requiredField<Int>("net.minecraft.class_3549\$class_3550", "field_15774", "I")
abstract class WrappedBakedModel(val wrapped: BakedModel) : BakedModel by wrapped, FabricBakedModel {
override fun isVanillaAdapter() = false
override fun emitItemQuads(stack: ItemStack, randomSupplier: Supplier<Random>, context: RenderContext) {
(wrapped as FabricBakedModel).emitItemQuads(stack, randomSupplier, context)
}
override fun emitBlockQuads(blockView: ExtendedBlockView, state: BlockState, pos: BlockPos, randomSupplier: Supplier<Random>, context: RenderContext) {
(wrapped as FabricBakedModel).emitBlockQuads(blockView, state, pos, randomSupplier, context)
}
}
class WrappedMeshModel(wrapped: BasicBakedModel, val mesh: Mesh) : WrappedBakedModel(wrapped) {
override fun emitBlockQuads(blockView: ExtendedBlockView, state: BlockState, pos: BlockPos, randomSupplier: Supplier<Random>, context: RenderContext) {
context.meshConsumer().accept(mesh)
}
companion object {
/**
* Converter for [BasicBakedModel] instances.
* @param state [BlockState] to use when querying [BakedModel]
* @param unshade undo vanilla diffuse lighting when unbaking the [BakedModel]
* @param noDiffuse disable diffuse lighting when baking the [Mesh]
* @param renderLayerOverride [BlockRenderLayer] to use instead of the one declared by the corresponding [Block]
*/
fun converter(state: BlockState, unshade: Boolean = false, noDiffuse: Boolean = true, renderLayerOverride: BlockRenderLayer? = null) = BakedModelConverter.of { model, _ ->
if (model is BasicBakedModel) {
val mesh = unbakeQuads(model, state, Random(42L), unshade).build(
layer = renderLayerOverride ?: state.block.renderLayer,
noDiffuse = noDiffuse,
flatLighting = !model.useAmbientOcclusion()
)
WrappedMeshModel(model, mesh)
} else null
}
}
}
class WrappedWeightedModel(wrapped: WeightedBakedModel, transformer: BakedModelConverter) : WrappedBakedModel(wrapped) {
val totalWeight = wrapped[WeightedBakedModel_totalWeight] as Int
val models = wrapped[WeightedBakedModel_models]!!.map { entry ->
Entry(transformer.convert(entry[WeightedBakedModelEntry_model]!!, transformer)!!, entry[WeightedPickerEntry_weight]!!)
}
override fun emitBlockQuads(blockView: ExtendedBlockView, state: BlockState, pos: BlockPos, randomSupplier: Supplier<Random>, context: RenderContext) {
(WeightedPicker.getRandom(randomSupplier.get(), models, totalWeight).model as FabricBakedModel).emitBlockQuads(blockView, state, pos, randomSupplier, context)
}
class Entry(val model: BakedModel, weight: Int) : WeightedPicker.Entry(weight)
companion object {
val converter = object : BakedModelConverter {
override fun convert(model: BakedModel, converter: BakedModelConverter) =
(model as? WeightedBakedModel)?.let { WrappedWeightedModel(it, converter) }
}
}
}