Rewrite dynamic resources once again
This commit is contained in:
parent
d8b86708e5
commit
5450a16aad
|
|
@ -1,22 +0,0 @@
|
|||
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
|
||||
import org.embeddedt.modernfix.duck.IExtendedModelManager;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(Minecraft.class)
|
||||
@ClientOnlyMixin
|
||||
public abstract class MinecraftMixin_ModelTicking {
|
||||
@Shadow public abstract ModelManager getModelManager();
|
||||
|
||||
@Inject(method = "tick", at = @At(value = "RETURN"))
|
||||
private void tickModels(CallbackInfo ci) {
|
||||
((IExtendedModelManager)this.getModelManager()).mfix$tick();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,32 @@
|
|||
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.block.model.BlockModel;
|
||||
import net.minecraft.client.resources.model.AtlasSet;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.BlockStateModelLoader;
|
||||
import net.minecraft.client.resources.model.ModelBakery;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.client.resources.model.ModelResourceLocation;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
|
||||
import org.embeddedt.modernfix.duck.IExtendedModelManager;
|
||||
import org.embeddedt.modernfix.dynamicresources.DynamicModelProvider;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
|
@ -26,9 +35,16 @@ import java.util.concurrent.Executor;
|
|||
|
||||
@Mixin(ModelManager.class)
|
||||
@ClientOnlyMixin
|
||||
public class ModelManagerMixin implements IExtendedModelManager {
|
||||
public class ModelManagerMixin {
|
||||
@Shadow private Map<ResourceLocation, BakedModel> bakedRegistry;
|
||||
|
||||
@Shadow private BakedModel missingModel;
|
||||
@Unique
|
||||
private DynamicModelProvider mfix$modelProvider;
|
||||
|
||||
@Unique
|
||||
private Map<ResourceLocation, AtlasSet.StitchResult> mfix$stitchResults;
|
||||
|
||||
@Inject(method = "<init>", at = @At("RETURN"))
|
||||
private void injectDummyBakedRegistry(CallbackInfo ci) {
|
||||
if(this.bakedRegistry == null) {
|
||||
|
|
@ -51,8 +67,31 @@ public class ModelManagerMixin implements IExtendedModelManager {
|
|||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mfix$tick() {
|
||||
@Inject(method = "loadModels", at = @At("HEAD"))
|
||||
private void saveStitchResults(ProfilerFiller profilerFiller, Map<ResourceLocation, AtlasSet.StitchResult> map, ModelBakery modelBakery, Object2IntMap<BlockState> object2IntMap, CallbackInfoReturnable<?> cir) {
|
||||
this.mfix$stitchResults = map;
|
||||
}
|
||||
|
||||
@Inject(method = "apply", at = @At("RETURN"))
|
||||
private void createModelProvider(CallbackInfo ci) {
|
||||
this.mfix$modelProvider = new DynamicModelProvider(
|
||||
null, // TODO
|
||||
this.missingModel,
|
||||
Minecraft.getInstance().getResourceManager(),
|
||||
this.mfix$stitchResults
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @author embeddedt
|
||||
* @reason use dynamic model system
|
||||
*/
|
||||
@Overwrite
|
||||
public BakedModel getModel(ModelResourceLocation modelLocation) {
|
||||
if(this.mfix$modelProvider != null) {
|
||||
return this.mfix$modelProvider.getModel(modelLocation);
|
||||
} else {
|
||||
return this.missingModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
package org.embeddedt.modernfix.dynamicresources;
|
||||
|
||||
import it.unimi.dsi.fastutil.Function;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ReferenceLinkedOpenHashMap;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
|
||||
/**
|
||||
* The Mojang Triple-based baked cache system is too slow to be hitting on every model retrieval, so
|
||||
* we need a fast, concurrency-safe wrapper on top.
|
||||
*/
|
||||
public class DynamicModelCache<K> {
|
||||
private final Reference2ReferenceLinkedOpenHashMap<K, BakedModel> cache = new Reference2ReferenceLinkedOpenHashMap<>();
|
||||
private final StampedLock lock = new StampedLock();
|
||||
private final Function<K, BakedModel> modelRetriever;
|
||||
private final boolean allowNulls;
|
||||
|
||||
public DynamicModelCache(Function<K, BakedModel> modelRetriever, boolean allowNulls) {
|
||||
this.modelRetriever = modelRetriever;
|
||||
this.allowNulls = allowNulls;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
long stamp = lock.writeLock();
|
||||
try {
|
||||
cache.clear();
|
||||
} finally {
|
||||
lock.unlock(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean needToPopulate(K state) {
|
||||
long stamp = lock.readLock();
|
||||
try {
|
||||
return !cache.containsKey(state);
|
||||
} finally {
|
||||
lock.unlock(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
private BakedModel getModelFromCache(K state) {
|
||||
long stamp = lock.readLock();
|
||||
try {
|
||||
return cache.get(state);
|
||||
} finally {
|
||||
lock.unlock(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
private BakedModel cacheModel(K state) {
|
||||
BakedModel model = modelRetriever.apply(state);
|
||||
|
||||
// Lock and modify our local, faster cache
|
||||
long stamp = lock.writeLock();
|
||||
|
||||
try {
|
||||
cache.putAndMoveToFirst(state, model);
|
||||
// TODO: choose less arbitrary number
|
||||
if(cache.size() >= 1000) {
|
||||
cache.removeLast();
|
||||
}
|
||||
} finally {
|
||||
lock.unlock(stamp);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public BakedModel get(K key) {
|
||||
BakedModel model = getModelFromCache(key);
|
||||
|
||||
if(model == null && (!allowNulls || needToPopulate(key))) {
|
||||
model = cacheModel(key);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,236 @@
|
|||
package org.embeddedt.modernfix.dynamicresources;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.gson.JsonObject;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||
import net.minecraft.client.renderer.block.model.BlockModel;
|
||||
import net.minecraft.client.renderer.block.model.BlockModelDefinition;
|
||||
import net.minecraft.client.resources.model.AtlasSet;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.BlockStateModelLoader;
|
||||
import net.minecraft.client.resources.model.MissingBlockModel;
|
||||
import net.minecraft.client.resources.model.ModelBakery;
|
||||
import net.minecraft.client.resources.model.ModelResourceLocation;
|
||||
import net.minecraft.client.resources.model.SpecialModels;
|
||||
import net.minecraft.client.resources.model.UnbakedModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.util.GsonHelper;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import org.embeddedt.modernfix.ModernFix;
|
||||
import org.embeddedt.modernfix.util.DynamicMap;
|
||||
|
||||
import java.io.Reader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Handles loading models dynamically, rather than at startup time.
|
||||
*/
|
||||
public class DynamicModelProvider {
|
||||
private final Map<ResourceLocation, UnbakedModel> internalModels;
|
||||
private final Cache<ResourceLocation, Optional<UnbakedModel>> loadedModels =
|
||||
private final LoadingCache<ResourceLocation, Optional<BlockStateModelLoader.LoadedModels>> loadedStateDefinitions =
|
||||
CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(3, TimeUnit.MINUTES)
|
||||
.maximumSize(1000)
|
||||
.concurrencyLevel(8)
|
||||
.softValues()
|
||||
.build();
|
||||
.build(new CacheLoader<>() {
|
||||
@Override
|
||||
public Optional<BlockStateModelLoader.LoadedModels> load(ResourceLocation key) {
|
||||
return loadBlockStateDefinition(key);
|
||||
}
|
||||
});
|
||||
|
||||
public DynamicModelProvider(Map<ResourceLocation, UnbakedModel> initialModels) {
|
||||
this.internalModels = initialModels;
|
||||
private final LoadingCache<ResourceLocation, Optional<UnbakedModel>> loadedBlockModels =
|
||||
CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(3, TimeUnit.MINUTES)
|
||||
.maximumSize(1000)
|
||||
.concurrencyLevel(8)
|
||||
.softValues()
|
||||
.build(new CacheLoader<>() {
|
||||
@Override
|
||||
public Optional<UnbakedModel> load(ResourceLocation key) {
|
||||
return loadBlockModel(key);
|
||||
}
|
||||
});
|
||||
|
||||
private final LoadingCache<ModelResourceLocation, Optional<UnbakedModel>> loadedUnbakedModels =
|
||||
CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(3, TimeUnit.MINUTES)
|
||||
.maximumSize(1000)
|
||||
.concurrencyLevel(8)
|
||||
.softValues()
|
||||
.build(new CacheLoader<>() {
|
||||
@Override
|
||||
public Optional<UnbakedModel> load(ModelResourceLocation key) {
|
||||
return loadModel(key);
|
||||
}
|
||||
});
|
||||
|
||||
private final LoadingCache<ModelResourceLocation, Optional<BakedModel>> loadedBakedModels =
|
||||
CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(3, TimeUnit.MINUTES)
|
||||
.maximumSize(1000)
|
||||
.concurrencyLevel(8)
|
||||
.softValues()
|
||||
.build(new CacheLoader<>() {
|
||||
@Override
|
||||
public Optional<BakedModel> load(ModelResourceLocation key) {
|
||||
return loadBakedModel(key);
|
||||
}
|
||||
});
|
||||
|
||||
private final Map<ModelResourceLocation, BakedModel> initialBakedRegistry;
|
||||
private final BakedModel missingModel;
|
||||
private final UnbakedModel unbakedMissingModel;
|
||||
private final Function<ResourceLocation, StateDefinition<Block, BlockState>> stateMapper;
|
||||
private final ResourceManager resourceManager;
|
||||
private final BlockStateModelLoader blockStateModelLoader;
|
||||
private final ModelBakery.TextureGetter textureGetter;
|
||||
private final DynamicMap<ResourceLocation, UnbakedModel> fakeUnbakedModelMap;
|
||||
private final DynamicResolver resolver;
|
||||
|
||||
public DynamicModelProvider(Map<ModelResourceLocation, BakedModel> initialBakedRegistry, BakedModel missingModel, ResourceManager resourceManager, Map<ResourceLocation, AtlasSet.StitchResult> atlasMap) {
|
||||
this.initialBakedRegistry = initialBakedRegistry;
|
||||
this.missingModel = missingModel;
|
||||
this.textureGetter = (mrl, material) -> {
|
||||
var atlas = atlasMap.get(material.atlasLocation());
|
||||
var sprite = atlas.getSprite(material.texture());
|
||||
if (sprite != null) {
|
||||
return sprite;
|
||||
} else {
|
||||
ModernFix.LOGGER.warn("Unable to find sprite '{}' referenced by model '{}'", material.texture(), mrl);
|
||||
return atlas.missing();
|
||||
}
|
||||
};
|
||||
this.stateMapper = BlockStateModelLoader.definitionLocationToBlockMapper();
|
||||
this.resourceManager = resourceManager;
|
||||
this.unbakedMissingModel = MissingBlockModel.missingModel();
|
||||
this.blockStateModelLoader = new BlockStateModelLoader(this.unbakedMissingModel);
|
||||
this.fakeUnbakedModelMap = new DynamicMap<>(ResourceLocation.class, key -> this.loadedBlockModels.getUnchecked(key).orElse(null));
|
||||
this.resolver = new DynamicResolver();
|
||||
}
|
||||
|
||||
public UnbakedModel getModel(ResourceLocation location) {
|
||||
try {
|
||||
return loadedModels.get(location, () -> Optional.ofNullable(loadModel(location))).orElse(null);
|
||||
} catch(ExecutionException e) {
|
||||
throw new RuntimeException(e.getCause());
|
||||
private Optional<BlockStateModelLoader.LoadedModels> loadBlockStateDefinition(ResourceLocation location) {
|
||||
StateDefinition<Block, BlockState> stateDefinition = this.stateMapper.apply(location);
|
||||
if(stateDefinition == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
List<Resource> resources = resourceManager.getResourceStack(ResourceLocation.fromNamespaceAndPath(location.getNamespace(), "blockstates/" + location.getPath() + ".json"));
|
||||
List<BlockStateModelLoader.LoadedBlockModelDefinition> loadedDefinitions = new ArrayList<>(resources.size());
|
||||
for(Resource resource : resources) {
|
||||
try(Reader reader = resource.openAsReader()) {
|
||||
JsonObject jsonObject = GsonHelper.parse(reader);
|
||||
BlockModelDefinition blockModelDefinition = BlockModelDefinition.fromJsonElement(jsonObject);
|
||||
loadedDefinitions.add(new BlockStateModelLoader.LoadedBlockModelDefinition(resource.sourcePackId(), blockModelDefinition));
|
||||
} catch(Exception e) {
|
||||
ModernFix.LOGGER.error("Failed to load blockstate definition {} from pack '{}'", location, resource.sourcePackId(), e);
|
||||
}
|
||||
}
|
||||
return Optional.of(this.blockStateModelLoader.loadBlockStateDefinitionStack(location, stateDefinition, loadedDefinitions));
|
||||
}
|
||||
|
||||
private BakedModel bakeModel(UnbakedModel model, ModelResourceLocation location) {
|
||||
synchronized (this) {
|
||||
this.resolver.clearResolver();
|
||||
model.resolveDependencies(this.resolver);
|
||||
var modelBakery = new ModelBakery(Map.of(location, model), this.fakeUnbakedModelMap, this.unbakedMissingModel);
|
||||
modelBakery.bakeModels(this.textureGetter);
|
||||
return modelBakery.getBakedTopLevelModels().get(location);
|
||||
}
|
||||
}
|
||||
|
||||
private UnbakedModel loadModel(ResourceLocation location) {
|
||||
return null; /* TODO :) */
|
||||
private Optional<BakedModel> loadBakedModel(ModelResourceLocation location) {
|
||||
var unbakedModel = this.loadedUnbakedModels.getUnchecked(location);
|
||||
return unbakedModel.map(model -> this.bakeModel(model, location));
|
||||
}
|
||||
|
||||
private Optional<UnbakedModel> loadBlockModel(ResourceLocation location) {
|
||||
if(location.equals(SpecialModels.BUILTIN_GENERATED)) {
|
||||
return Optional.of(SpecialModels.GENERATED_MARKER);
|
||||
} else if(location.equals(SpecialModels.BUILTIN_BLOCK_ENTITY)) {
|
||||
return Optional.of(SpecialModels.BLOCK_ENTITY_MARKER);
|
||||
}
|
||||
var resource = this.resourceManager.getResource(ResourceLocation.fromNamespaceAndPath(location.getNamespace(), "models/" + location.getPath() + ".json"));
|
||||
if(resource.isPresent()) {
|
||||
try(Reader reader = resource.get().openAsReader()) {
|
||||
BlockModel blockModel = BlockModel.fromStream(reader);
|
||||
blockModel.name = location.toString();
|
||||
return Optional.of(blockModel);
|
||||
} catch(Exception e) {
|
||||
ModernFix.LOGGER.error("Failed to load block model {} from '{}'", location, resource.get().sourcePackId(), e);
|
||||
return Optional.empty();
|
||||
}
|
||||
} else {
|
||||
ModernFix.LOGGER.warn("Model '{}' does not exist in any resource packs", location);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<UnbakedModel> loadModel(ModelResourceLocation location) {
|
||||
if (location.variant().equals(ModelResourceLocation.INVENTORY_VARIANT)) {
|
||||
return this.loadedBlockModels.getUnchecked(ResourceLocation.fromNamespaceAndPath(location.id().getNamespace(), "item/" + location.id().getPath()));
|
||||
} else if (location.variant().equals("standalone") || location.variant().equals("fabric_resource")) {
|
||||
return this.loadedBlockModels.getUnchecked(location.id());
|
||||
} else {
|
||||
var optLoadedModels = this.loadedStateDefinitions.getUnchecked(location.id());
|
||||
return optLoadedModels.map(loadedModels -> {
|
||||
var loadedModel = loadedModels.models().get(location);
|
||||
if(loadedModel != null) {
|
||||
return loadedModel.model();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public BakedModel getModel(ModelResourceLocation location) {
|
||||
return this.loadedBakedModels.getUnchecked(location).orElse(this.missingModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the Mojang impl but with some changes to make it slightly more efficient.
|
||||
*/
|
||||
private class DynamicResolver implements UnbakedModel.Resolver {
|
||||
private final Set<ResourceLocation> stack = new ObjectOpenHashSet<>(4);
|
||||
private final Set<ResourceLocation> resolvedModels = new ObjectOpenHashSet<>();
|
||||
|
||||
@Override
|
||||
public UnbakedModel resolve(ResourceLocation resourceLocation) {
|
||||
if (this.stack.contains(resourceLocation)) {
|
||||
ModernFix.LOGGER.warn("Detected model loading loop: {}->{}", this.stacktraceToString(), resourceLocation);
|
||||
return DynamicModelProvider.this.unbakedMissingModel;
|
||||
} else {
|
||||
UnbakedModel unbakedModel = DynamicModelProvider.this.loadedBlockModels.getUnchecked(resourceLocation).orElse(DynamicModelProvider.this.unbakedMissingModel);
|
||||
if (this.resolvedModels.add(resourceLocation)) {
|
||||
this.stack.add(resourceLocation);
|
||||
unbakedModel.resolveDependencies(this);
|
||||
this.stack.remove(resourceLocation);
|
||||
}
|
||||
|
||||
return unbakedModel;
|
||||
}
|
||||
}
|
||||
|
||||
private String stacktraceToString() {
|
||||
return this.stack.stream().map(ResourceLocation::toString).collect(Collectors.joining("->"));
|
||||
}
|
||||
|
||||
public void clearResolver() {
|
||||
this.stack.clear();
|
||||
this.resolvedModels.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,45 +11,6 @@ import net.minecraft.world.level.block.state.properties.Property;
|
|||
import java.util.*;
|
||||
|
||||
public class ModelBakeryHelpers {
|
||||
/**
|
||||
* The maximum number of baked models kept in memory at once.
|
||||
*/
|
||||
public static final int MAX_BAKED_MODEL_COUNT = 10000;
|
||||
/**
|
||||
* The maximum number of unbaked models kept in memory at once.
|
||||
*/
|
||||
public static final int MAX_UNBAKED_MODEL_COUNT = 10000;
|
||||
/**
|
||||
* The time in seconds after which a model becomes eligible for eviction if not used.
|
||||
*/
|
||||
public static final int MAX_MODEL_LIFETIME_SECS = 300;
|
||||
|
||||
/**
|
||||
* These folders will have all textures stitched onto the atlas when dynamic resources is enabled.
|
||||
*/
|
||||
public static String[] getExtraTextureFolders() {
|
||||
return new String[] {
|
||||
"attachment",
|
||||
"bettergrass",
|
||||
"block",
|
||||
"blocks",
|
||||
"cape",
|
||||
"entity/bed",
|
||||
"entity/chest",
|
||||
"item",
|
||||
"items",
|
||||
"model",
|
||||
"models",
|
||||
"part",
|
||||
"pipe",
|
||||
"ropebridge",
|
||||
"runes",
|
||||
"solid_block",
|
||||
"spell_effect",
|
||||
"spell_projectile"
|
||||
};
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>, V extends T> BlockState setPropertyGeneric(BlockState state, Property<T> prop, Object o) {
|
||||
return state.setValue(prop, (V)o);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package org.embeddedt.modernfix.dynamicresources;
|
||||
|
||||
import net.minecraft.client.renderer.block.model.BlockFaceUV;
|
||||
|
||||
public class UVController {
|
||||
public static final ThreadLocal<Boolean> useDummyUv = ThreadLocal.withInitial(() -> Boolean.FALSE);
|
||||
public static final BlockFaceUV dummyUv = new BlockFaceUV(new float[4], 0);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user