[WIP] major rewrite, grass and leaves working already

This commit is contained in:
octarine-noise
2021-05-06 22:40:32 +02:00
parent 09ccb83e8b
commit f44d2a7a50
96 changed files with 2348 additions and 3531 deletions

View File

@@ -1,95 +0,0 @@
package mods.octarinecore.client.resource
import mods.betterfoliage.resource.Identifier
import mods.betterfoliage.resource.Sprite
import mods.betterfoliage.util.map
import net.minecraft.client.renderer.texture.AtlasTexture
import net.minecraft.client.renderer.texture.MissingTextureSprite
import net.minecraft.profiler.IProfiler
import net.minecraft.resources.IResourceManager
import java.util.*
import java.util.concurrent.CompletableFuture
/**
* Main entry point to atlas manipulation. Called from mixins that wrap [AtlasTexture.stitch] calls.
*
* 1. All registered providers receive an [AsyncSpriteProvider.setup] call. Providers can set up their
* processing chain at this point, but should not do anything yet except configuration and housekeeping.
* 2. The [CompletableFuture] of the stitch source finishes, starting the "discovery" phase. Providers
* may register sprites in the [AtlasFuture].
* 3. After all providers finish their discovery, the atlas is stitched.
* 4. The [AtlasFuture] finishes, starting the "cleanup" phase. All [AtlasFuture.runAfter] and
* [AtlasFuture.mapAfter] tasks are processed.
* 5. After all providers finish their cleanup, we return to the original code path.
*/
class AsnycSpriteProviderManager<SOURCE: Any>(val profilerSection: String) {
val providers = mutableListOf<AsyncSpriteProvider<SOURCE>>()
/**
* Needed in order to keep the actual [AtlasTexture.stitch] call in the original method, in case
* other modders want to modify it too.
*/
class StitchWrapper(val idList: Iterable<Identifier>, val onComplete: (AtlasTexture.SheetData)->Unit) {
fun complete(sheet: AtlasTexture.SheetData) = onComplete(sheet)
}
var currentAtlas: AtlasFuture? = null
var currentPhases: List<StitchPhases> = emptyList()
@Suppress("UNCHECKED_CAST")
fun prepare(sourceObj: Any, manager: IResourceManager, idList: Iterable<Identifier>, profiler: IProfiler): Set<Identifier> {
profiler.startSection(profilerSection)
val source = CompletableFuture<SOURCE>()
currentAtlas = AtlasFuture(idList)
currentPhases = providers.map { it.setup(manager, source, currentAtlas!!) }
source.complete(sourceObj as SOURCE)
currentPhases.forEach { it.discovery.get() }
return currentAtlas!!.idSet
}
fun finish(sheetData: AtlasTexture.SheetData, profiler: IProfiler): AtlasTexture.SheetData {
currentAtlas!!.complete(sheetData)
currentPhases.forEach { it.cleanup.get() }
currentAtlas = null
currentPhases = emptyList()
profiler.endSection()
return sheetData
}
}
/**
* Provides a way for [AsyncSpriteProvider]s to register sprites to receive [CompletableFuture]s.
* Tracks sprite ids that need to be stitched.
*/
class AtlasFuture(initial: Iterable<Identifier>) {
val idSet = Collections.synchronizedSet(mutableSetOf<Identifier>().apply { addAll(initial) })
protected val sheet = CompletableFuture<AtlasTexture.SheetData>()
protected val finished = CompletableFuture<Void>()
fun complete(sheetData: AtlasTexture.SheetData) {
sheet.complete(sheetData)
finished.complete(null)
}
fun sprite(id: String) = sprite(Identifier(id))
fun sprite(id: Identifier): CompletableFuture<Sprite> {
idSet.add(id)
return sheet.map { sheetData -> sheetData.sprites.find { it.name == id } ?: throw IllegalStateException("Atlas does not contain $id") }
}
val missing = sheet.map { sheetData -> sheetData.sprites.find { it.name == MissingTextureSprite.getLocation() } }
fun <T> mapAfter(supplier: ()->T): CompletableFuture<T> = finished.map{ supplier() }
fun runAfter(action: ()->Unit): CompletableFuture<Void> = finished.thenRun(action)
}
class StitchPhases(
val discovery: CompletableFuture<Void>,
val cleanup: CompletableFuture<Void>
)
interface AsyncSpriteProvider<SOURCE: Any> {
fun setup(manager: IResourceManager, source: CompletableFuture<SOURCE>, atlas: AtlasFuture): StitchPhases
}

View File

@@ -0,0 +1,126 @@
package mods.betterfoliage.resource.discovery
import mods.betterfoliage.ModelDefinitionsLoadedEvent
import mods.betterfoliage.render.bakeSpecial
import mods.betterfoliage.util.Atlas
import mods.betterfoliage.util.HasLogger
import mods.betterfoliage.util.Invalidator
import mods.betterfoliage.util.SimpleInvalidator
import mods.betterfoliage.util.asBlockMaterial
import net.minecraft.client.renderer.model.IBakedModel
import net.minecraft.client.renderer.model.IModelTransform
import net.minecraft.client.renderer.model.IUnbakedModel
import net.minecraft.client.renderer.model.Material
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraft.client.renderer.model.VariantList
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.ResourceLocation
import net.minecraftforge.client.event.ModelBakeEvent
import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.common.ForgeConfig
import net.minecraftforge.eventbus.api.SubscribeEvent
import net.minecraftforge.fml.loading.progress.StartupMessageManager
import org.apache.logging.log4j.Level.INFO
import org.apache.logging.log4j.LogManager
import java.lang.ref.WeakReference
import java.util.function.Function
interface ModelDiscovery {
fun onModelsLoaded(
bakery: ModelBakery,
sprites: MutableSet<ResourceLocation>,
replacements: MutableMap<ResourceLocation, ModelBakeKey>
)
}
@FunctionalInterface
interface ModelBakeKey {
fun replace(
location: ResourceLocation,
unbaked: IUnbakedModel,
transform: IModelTransform,
bakery: ModelBakery,
spriteGetter: Function<Material, TextureAtlasSprite>
): IBakedModel? = unbaked.bakeModel(bakery, spriteGetter, transform, location)
}
interface ModelWrapperKey : ModelBakeKey {
override fun replace(
location: ResourceLocation,
unbaked: IUnbakedModel,
transform: IModelTransform,
bakery: ModelBakery,
spriteGetter: Function<Material, TextureAtlasSprite>
): IBakedModel? {
val baked = super.replace(location, unbaked, transform, bakery, spriteGetter) ?: return null
val sprites = { res: ResourceLocation -> spriteGetter.apply(res.asBlockMaterial) }
return replace(location, baked, sprites)
}
fun replace(
location: ResourceLocation,
wrapped: IBakedModel,
sprites: (ResourceLocation) -> TextureAtlasSprite
) = replace(wrapped)
fun replace(wrapped: IBakedModel) = wrapped
}
object BakeWrapperManager : Invalidator, HasLogger() {
val discoverers = mutableListOf<ModelDiscovery>()
override val callbacks = mutableListOf<WeakReference<()->Unit>>()
val modelsValid = SimpleInvalidator()
val spritesValid = SimpleInvalidator()
private val replacements = mutableMapOf<ResourceLocation, ModelBakeKey>()
private val sprites = mutableSetOf<ResourceLocation>()
@SubscribeEvent
fun handleModelLoad(event: ModelDefinitionsLoadedEvent) {
modelsValid.invalidate()
StartupMessageManager.addModMessage("BetterFoliage: discovering models")
logger.log(INFO, "starting model discovery (${discoverers.size} listeners)")
discoverers.forEach { listener ->
val replacementsLocal = mutableMapOf<ResourceLocation, ModelBakeKey>()
listener.onModelsLoaded(event.bakery, sprites, replacementsLocal)
replacements.putAll(replacementsLocal)
}
}
@SubscribeEvent
fun handleStitch(event: TextureStitchEvent.Pre) {
if (event.map.textureLocation == Atlas.BLOCKS.resourceId) {
logger.log(INFO, "Adding ${sprites.size} sprites to block atlas")
spritesValid.invalidate()
sprites.forEach { event.addSprite(it) }
sprites.clear()
}
}
@SubscribeEvent
fun handleModelBake(event: ModelBakeEvent) {
replacements.clear()
}
fun onBake(
unbaked: IUnbakedModel,
bakery: ModelBakery,
spriteGetter: Function<Material, TextureAtlasSprite>,
transform: IModelTransform,
location: ResourceLocation
): IBakedModel? {
// bake replacement if available
replacements[location]?.let { replacement ->
detailLogger.log(INFO, "Baking replacement for [${unbaked::class.java.simpleName}] $location -> $replacement")
return replacement.replace(location, unbaked, transform, bakery, spriteGetter)
}
// container model support
if (unbaked is VariantList) unbaked.bakeSpecial(bakery, spriteGetter)?.let {
detailLogger.log(INFO, "Wrapping container [${unbaked::class.java.simpleName}] $location")
return it
}
return unbaked.bakeModel(bakery, spriteGetter, transform, location)
}
}

View File

@@ -0,0 +1,19 @@
package mods.betterfoliage.resource.discovery
import mods.betterfoliage.Client
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraft.util.ResourceLocation
class BlockTypeCache {
val leaf = mutableSetOf<BlockState>()
val grass = mutableSetOf<BlockState>()
val dirt = mutableSetOf<BlockState>()
companion object : ModelDiscovery {
override fun onModelsLoaded(bakery: ModelBakery, sprites: MutableSet<ResourceLocation>, replacements: MutableMap<ResourceLocation, ModelBakeKey>
) {
Client.blockTypes = BlockTypeCache()
}
}
}

View File

@@ -1,143 +0,0 @@
package mods.betterfoliage.resource.discovery
import com.google.common.base.Joiner
import mods.betterfoliage.render.old.BlockCtx
import mods.octarinecore.client.resource.AsyncSpriteProvider
import mods.octarinecore.client.resource.AtlasFuture
import mods.octarinecore.client.resource.StitchPhases
import mods.betterfoliage.util.Int3
import mods.betterfoliage.config.IBlockMatcher
import mods.betterfoliage.config.ModelTextureList
import mods.betterfoliage.util.HasLogger
import mods.betterfoliage.util.findFirst
import mods.betterfoliage.util.plus
import mods.betterfoliage.util.sinkAsync
import mods.betterfoliage.util.stripStart
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.BlockModelShapes
import net.minecraft.client.renderer.model.BlockModel
import net.minecraft.client.renderer.model.IUnbakedModel
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraft.client.renderer.model.ModelResourceLocation
import net.minecraft.client.renderer.model.VariantList
import net.minecraft.client.renderer.texture.MissingTextureSprite
import net.minecraft.resources.IResourceManager
import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockReader
import net.minecraftforge.registries.ForgeRegistries
import java.util.concurrent.CompletableFuture
interface ModelRenderRegistry<T> {
operator fun get(ctx: BlockCtx) = get(ctx.state, ctx.world, ctx.pos)
operator fun get(ctx: BlockCtx, offset: Int3) = get(ctx.state(offset), ctx.world, ctx.pos + offset)
operator fun get(state: BlockState, world: IBlockReader, pos: BlockPos): T?
}
abstract class ModelRenderRegistryRoot<T> : ModelRenderRegistry<T> {
val registries = mutableListOf<ModelRenderRegistry<T>>()
override fun get(state: BlockState, world: IBlockReader, pos: BlockPos) = registries.findFirst { it[state, world, pos] }
}
/**
* Information about a single BlockState and all the IUnbakedModel it could render as.
*/
class ModelDiscoveryContext(
bakery: ModelBakery,
val state: BlockState,
val modelId: ModelResourceLocation
) {
val models = bakery.unwrapVariants(bakery.getUnbakedModel(modelId) to modelId)
.filter { it.second != bakery.getUnbakedModel(ModelBakery.MODEL_MISSING) }
fun ModelBakery.unwrapVariants(modelAndLoc: Pair<IUnbakedModel, ResourceLocation>): List<Pair<IUnbakedModel, ResourceLocation>> = when(val model = modelAndLoc.first) {
is VariantList -> model.variantList.flatMap { variant -> unwrapVariants(getUnbakedModel(variant.modelLocation) to variant.modelLocation) }
is BlockModel -> listOf(modelAndLoc)
else -> emptyList()
}
}
abstract class ModelDiscovery<T> : HasLogger, AsyncSpriteProvider<ModelBakery>, ModelRenderRegistry<T> {
var modelData: Map<BlockState, T> = emptyMap()
protected set
override fun get(state: BlockState, world: IBlockReader, pos: BlockPos) = modelData[state]
abstract fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<T>?
override fun setup(manager: IResourceManager, bakeryF: CompletableFuture<ModelBakery>, atlas: AtlasFuture): StitchPhases {
val modelDataTemp = mutableMapOf<BlockState, CompletableFuture<T>>()
return StitchPhases(
discovery = bakeryF.sinkAsync { bakery ->
var errors = 0
bakery.iterateModels { ctx ->
try {
processModel(ctx, atlas)?.let { modelDataTemp[ctx.state] = it }
} catch (e: Exception) {
errors++
}
}
log("${modelDataTemp.size} BlockStates discovered, $errors errors")
},
cleanup = atlas.runAfter {
modelData = modelDataTemp.filterValues { !it.isCompletedExceptionally }.mapValues { it.value.get() }
val errors = modelDataTemp.values.filter { it.isCompletedExceptionally }.size
log("${modelData.size} BlockStates loaded, $errors errors")
}
)
}
fun ModelBakery.iterateModels(func: (ModelDiscoveryContext)->Unit) {
ForgeRegistries.BLOCKS.flatMap { block ->
block.stateContainer.validStates.map { state -> state to BlockModelShapes.getModelLocation(state) }
}.forEach { (state, stateModelResource) ->
func(ModelDiscoveryContext(this, state, stateModelResource))
}
}
}
abstract class ConfigurableModelDiscovery<T> : ModelDiscovery<T>() {
abstract val matchClasses: IBlockMatcher
abstract val modelTextures: List<ModelTextureList>
abstract fun processModel(state: BlockState, textures: List<ResourceLocation>, atlas: AtlasFuture): CompletableFuture<T>?
override fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<T>? {
val matchClass = matchClasses.matchingClass(ctx.state.block) ?: return null
log("block state ${ctx.state}")
log(" class ${ctx.state.block.javaClass.name} matches ${matchClass.name}")
if (ctx.models.isEmpty()) {
log(" no models found")
return null
}
ctx.models.filter { it.first is BlockModel }.forEach { (model, location) ->
model as BlockModel
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.resolveTextureName(it).textureLocation }
val texMapString = Joiner.on(", ").join(textures.map { "${it.first}=${it.second}" })
log(" sprites [$texMapString]")
if (textures.all { it.second != MissingTextureSprite.getLocation() }) {
// found a valid model (all required textures exist)
return processModel(ctx.state, textures.map { it.second}, atlas)
}
}
}
return null
}
}
fun Pair<BlockModel, ResourceLocation>.derivesFrom(targetLocation: ResourceLocation): Boolean {
if (second.stripStart("models/") == targetLocation) return true
if (first.parent != null && first.parentLocation != null)
return Pair(first.parent!!, first.parentLocation!!).derivesFrom(targetLocation)
return false
}

View File

@@ -0,0 +1,121 @@
package mods.betterfoliage.resource.discovery
import com.google.common.base.Joiner
import mods.betterfoliage.config.IBlockMatcher
import mods.betterfoliage.config.ModelTextureList
import mods.betterfoliage.util.HasLogger
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.BlockModelShapes
import net.minecraft.client.renderer.model.BlockModel
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraft.client.renderer.model.VariantList
import net.minecraft.client.renderer.model.multipart.Multipart
import net.minecraft.client.renderer.texture.MissingTextureSprite
import net.minecraft.util.ResourceLocation
import net.minecraftforge.registries.ForgeRegistries
import org.apache.logging.log4j.Level
abstract class ModelReplacer : HasLogger(), ModelDiscovery {
override fun onModelsLoaded(
bakery: ModelBakery,
sprites: MutableSet<ResourceLocation>,
replacements: MutableMap<ResourceLocation, ModelBakeKey>
) {
ForgeRegistries.BLOCKS
.flatMap { block -> block.stateContainer.validStates }
.forEach { state ->
val location = BlockModelShapes.getModelLocation(state)
try {
val hasReplaced = processModel(bakery, state, location, sprites, replacements)
} catch (e: Exception) {
logger.log(Level.WARN, "Discovery error in $location", e)
}
}
}
open fun processModel(
bakery: ModelBakery,
state: BlockState,
location: ResourceLocation,
sprites: MutableSet<ResourceLocation>,
replacements: MutableMap<ResourceLocation, ModelBakeKey>
): Boolean {
// built-in support for container models
return when (val model = bakery.getUnbakedModel(location)) {
is VariantList -> {
val hasReplaced = model.variantList.fold(false) { hasReplaced, variant ->
processModel(bakery, state, variant.modelLocation, sprites, replacements) || hasReplaced
}
if (hasReplaced) replacements[location]
hasReplaced
}
is Multipart -> model.variants.fold(false) { hasReplaced, variantList ->
variantList.variantList.fold(false) { hasReplaced, variant ->
processModel(bakery, state, variant.modelLocation, sprites, replacements) || hasReplaced
} || hasReplaced
}
else -> false
}
}
}
abstract class ConfigurableModelReplacer : ModelReplacer() {
abstract val matchClasses: IBlockMatcher
abstract val modelTextures: List<ModelTextureList>
abstract fun processModel(
state: BlockState,
location: ResourceLocation,
textureMatch: List<ResourceLocation>,
sprites: MutableSet<ResourceLocation>,
replacements: MutableMap<ResourceLocation, ModelBakeKey>
): Boolean
override fun processModel(
bakery: ModelBakery,
state: BlockState,
location: ResourceLocation,
sprites: MutableSet<ResourceLocation>,
replacements: MutableMap<ResourceLocation, ModelBakeKey>
): Boolean {
val model = bakery.getUnbakedModel(location)
if (model is BlockModel) {
val matchClass = matchClasses.matchingClass(state.block) ?: return false
detailLogger.log(Level.INFO, "block state $state")
detailLogger.log(Level.INFO, " model $location")
replacements[location]?.let { existing ->
detailLogger.log(Level.INFO, " already processed as $existing")
return true
}
detailLogger.log(Level.INFO, " class ${state.block.javaClass.name} matches ${matchClass.name}")
modelTextures
.filter { matcher -> bakery.modelDerivesFrom(model, location, matcher.modelLocation) }
.forEach { match ->
detailLogger.log(Level.INFO, " model ${model} matches ${match.modelLocation}")
val materials = match.textureNames.map { it to model.resolveTextureName(it) }
val texMapString = Joiner.on(", ").join(materials.map { "${it.first}=${it.second.textureLocation}" })
detailLogger.log(Level.INFO, " sprites [$texMapString]")
if (materials.all { it.second.textureLocation != MissingTextureSprite.getLocation() }) {
// found a valid model (all required textures exist)
if (processModel(state, location, materials.map { it.second.textureLocation }, sprites, replacements))
return true
}
}
}
return super.processModel(bakery, state, location, sprites, replacements)
}
}
fun ModelBakery.modelDerivesFrom(model: BlockModel, location: ResourceLocation, target: ResourceLocation): Boolean =
if (location == target) true
else model.parentLocation
?.let { getUnbakedModel(it) as? BlockModel }
?.let { parent -> modelDerivesFrom(parent, model.parentLocation!!, target) }
?: false