Optimise villager rendering by first combining the textures so we can render the model once.

This commit is contained in:
Sollace 2022-05-23 23:19:53 +02:00
parent 8911cbf337
commit 6cb383952c
2 changed files with 111 additions and 64 deletions

View file

@ -2,23 +2,16 @@ package com.minelittlepony.client.render.entity.npc;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
import net.minecraft.client.render.entity.feature.VillagerResourceMetadata;
import net.minecraft.client.render.entity.feature.VillagerResourceMetadata.HatType;
import net.minecraft.client.render.entity.model.EntityModel;
import net.minecraft.client.render.entity.model.ModelWithHat;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.LivingEntity;
import net.minecraft.resource.ResourceManager;
import net.minecraft.resource.Resource;
import net.minecraft.util.Identifier;
import net.minecraft.util.Util;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.registry.DefaultedRegistry;
import net.minecraft.util.registry.Registry;
import net.minecraft.village.VillagerData;
import net.minecraft.village.VillagerDataContainer;
@ -28,9 +21,9 @@ import net.minecraft.village.VillagerType;
import com.minelittlepony.client.model.IPonyModel;
import com.minelittlepony.client.render.IPonyRenderContext;
import com.minelittlepony.client.render.entity.feature.AbstractPonyFeature;
import com.minelittlepony.client.util.render.TextureFlattener;
import java.io.IOException;
import java.util.Map;
import java.util.*;
class NpcClothingFeature<
T extends LivingEntity & VillagerDataContainer,
@ -45,13 +38,8 @@ class NpcClothingFeature<
a.put(5, new Identifier("diamond"));
});
private final Object2ObjectMap<VillagerType, HatType> typeHatCache = new Object2ObjectOpenHashMap<>();
private final Object2ObjectMap<VillagerProfession, HatType> profHatCache = new Object2ObjectOpenHashMap<>();
private final String entityType;
private final ResourceManager resourceManager = MinecraftClient.getInstance().getResourceManager();
public NpcClothingFeature(C context, String type) {
super(context);
entityType = type;
@ -60,72 +48,61 @@ class NpcClothingFeature<
public static Identifier getClothingTexture(VillagerDataContainer entity, String entityType) {
VillagerProfession profession = entity.getVillagerData().getProfession();
return createTexture("minelittlepony", "profession", entityType, Registry.VILLAGER_PROFESSION.getId(profession));
return createTexture("minelittlepony", entityType, "profession", Registry.VILLAGER_PROFESSION.getId(profession));
}
public static Identifier createTexture(String namespace, String type, String entityType, Identifier profession) {
return new Identifier(namespace, String.format("textures/entity/%s/%s/%s.png", entityType, type, profession.getPath()));
public static Identifier createTexture(String namespace, String entityType, String category, Identifier identifier) {
return new Identifier(namespace, String.format("textures/entity/%s/%s/%s.png", entityType, category, identifier.getPath()));
}
public Identifier createTexture(String category, Identifier identifier) {
return createTexture("minelittlepony", entityType, category, identifier);
}
@Override
public void render(MatrixStack matrixStack, VertexConsumerProvider provider, int i, T entity, float f, float g, float h, float j, float k, float l) {
if (!entity.isInvisible()) {
if (entity.isInvisible()) {
return;
}
VillagerData data = entity.getVillagerData();
VillagerType type = data.getType();
VillagerProfession profession = data.getProfession();
VillagerData data = entity.getVillagerData();
M entityModel = getContextModel();
HatType typeHatLayer = getHatType(typeHatCache, "type", Registry.VILLAGER_TYPE, type);
HatType profHatLayer = getHatType(profHatCache, "profession", Registry.VILLAGER_PROFESSION, profession);
M entityModel = getContextModel();
entityModel.setHatVisible(
profHatLayer == VillagerResourceMetadata.HatType.NONE
|| (profHatLayer == VillagerResourceMetadata.HatType.PARTIAL && typeHatLayer != VillagerResourceMetadata.HatType.FULL)
);
Identifier typeSkin = findTexture("type", Registry.VILLAGER_TYPE.getId(type));
if (entity.isBaby() || data.getProfession() == VillagerProfession.NONE) {
Identifier typeSkin = createTexture("type", Registry.VILLAGER_TYPE.getId(data.getType()));
renderModel(entityModel, typeSkin, matrixStack, provider, i, entity, 1, 1, 1);
entityModel.setHatVisible(true);
if (profession != VillagerProfession.NONE && !entity.isBaby()) {
Identifier professionSkin = findTexture("profession", Registry.VILLAGER_PROFESSION.getId(profession));
renderModel(entityModel, professionSkin, matrixStack, provider, i, entity, 1, 1, 1);
if (profession != VillagerProfession.NITWIT) {
Identifier levelSkin = findTexture("profession_level", LEVEL_TO_ID.get(MathHelper.clamp(data.getLevel(), 1, LEVEL_TO_ID.size())));
renderModel(entityModel, levelSkin, matrixStack, provider, i, entity, 1, 1, 1);
}
}
} else {
renderModel(entityModel, getMergedTexture(data), matrixStack, provider, i, entity, 1, 1, 1);
}
}
public <K> VillagerResourceMetadata.HatType getHatType(Object2ObjectMap<K, HatType> cache, String type, DefaultedRegistry<K> registry, K key) {
if (cache.containsKey(key)) {
return cache.get(key); // People often complain that villagers cause lag,
// so let's do better than Mojang and rather NOT go
// through all the lambda generations if we can avoid it.
public Identifier getMergedTexture(VillagerData data) {
VillagerType type = data.getType();
VillagerProfession profession = data.getProfession();
int level = MathHelper.clamp(data.getLevel(), 1, LEVEL_TO_ID.size());
Identifier typeId = Registry.VILLAGER_TYPE.getId(type);
Identifier profId = Registry.VILLAGER_PROFESSION.getId(profession);
Identifier key = new Identifier((typeId + "/" + profId + "/" + level).replace(':', '_'));
if (MinecraftClient.getInstance().getTextureManager().getOrDefault(key, null) == null) {
TextureFlattener.flatten(computeTextures(type, profession, typeId, profId, level), (Identifier)key);
}
return loadHatType(cache, type, registry, key);
return key;
}
private <K> VillagerResourceMetadata.HatType loadHatType(Map<K, HatType> cache, String type, DefaultedRegistry<K> registry, K key) {
return cache.computeIfAbsent(key, k -> {
try (Resource res = resourceManager.getResource(findTexture(type, registry.getId(k)))) {
VillagerResourceMetadata meta = res.getMetadata(VillagerResourceMetadata.READER);
if (meta != null) {
return meta.getHatType();
}
} catch (IOException e) { }
return HatType.NONE;
});
}
private List<Identifier> computeTextures(VillagerType type, VillagerProfession profession, Identifier typeId, Identifier profId, int level) {
List<Identifier> skins = new ArrayList<>();
public Identifier findTexture(String category, Identifier identifier) {
return new Identifier("minelittlepony", "textures/entity/" + entityType + "/" + category + "/" + identifier.getPath() + ".png");
skins.add(createTexture("type", typeId));
skins.add(createTexture("profession", Registry.VILLAGER_PROFESSION.getId(profession)));
if (profession != VillagerProfession.NITWIT) {
skins.add(createTexture("profession_level", LEVEL_TO_ID.get(level)));
}
return skins;
}
}

View file

@ -0,0 +1,70 @@
package com.minelittlepony.client.util.render;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.texture.*;
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
import com.google.common.base.Preconditions;
import com.mojang.blaze3d.platform.TextureUtil;
import com.mojang.blaze3d.systems.RenderSystem;
import java.io.IOException;
import java.util.List;
public class TextureFlattener {
public static void flatten(List<Identifier> textures, Identifier output) {
Preconditions.checkArgument(textures.size() > 0, "Must have at least one image to flatten");
MinecraftClient.getInstance().getTextureManager().registerTexture(output, new ResourceTexture(output) {
@Override
public void load(ResourceManager resManager) throws IOException {
NativeImage image = null;
for (int i = 0; i < textures.size(); i++) {
TextureData data = TextureData.load(resManager, textures.get(0));
data.checkException();
if (image == null) {
image = data.getImage();
} else {
copyOver(data.getImage(), image);
}
}
if (!RenderSystem.isOnRenderThreadOrInit()) {
final NativeImage i = image;
RenderSystem.recordRenderCall(() -> upload(i));
} else {
upload(image);
}
}
private void upload(NativeImage image) {
TextureUtil.prepareImage(getGlId(), 0, image.getWidth(), image.getHeight());
image.upload(0, 0, 0, 0, 0, image.getWidth(), image.getHeight(), false, false, false, true);
}
});
}
public static void copyOver(NativeImage from, NativeImage to) {
copyOver(from, to, 0, 0,
Math.min(from.getWidth(), to.getWidth()),
Math.min(from.getHeight(), to.getHeight())
);
}
public static void copyOver(NativeImage from, NativeImage to, int x, int y, int w, int h) {
for (; x < w; x++) {
for (; y < h; y++) {
copy(from, to, x, y);
}
}
}
public static void copy(NativeImage from, NativeImage to, int x, int y) {
int color = from.getColor(x, y);
if (NativeImage.getAlpha(color) > 0) {
to.setColor(x, y, color);
}
}
}