1
0
mirror of https://github.com/GeyserMC/Rainbow.git synced 2025-12-19 14:59:16 +00:00

Re-use mapped geometry when the textures and geometry is the same, let GeometryRenderer return TextureHolder, fix client geometry rendering

This commit is contained in:
Eclipse
2025-10-18 11:02:51 +00:00
parent 975d0c3d0d
commit bf07311112
24 changed files with 451 additions and 164 deletions

View File

@@ -1,24 +1,9 @@
package org.geysermc.rainbow.client.render; package org.geysermc.rainbow.client.render;
import com.mojang.blaze3d.buffers.GpuBuffer; import net.minecraft.resources.ResourceLocation;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.systems.CommandEncoder;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.GpuTexture;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.pip.OversizedItemRenderer;
import net.minecraft.client.gui.render.state.GuiItemRenderState;
import net.minecraft.client.gui.render.state.GuiRenderState;
import net.minecraft.client.gui.render.state.pip.OversizedItemRenderState;
import net.minecraft.client.renderer.item.TrackingItemStackRenderState;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; import org.geysermc.rainbow.mapping.geometry.GeometryRenderer;
import org.geysermc.rainbow.client.mixin.PictureInPictureRendererAccessor; import org.geysermc.rainbow.mapping.texture.TextureHolder;
import org.joml.Matrix3x2fStack;
import java.util.Objects;
// TODO maybe just use this even for normal 2D items, not sure, could be useful for composite models and stuff // TODO maybe just use this even for normal 2D items, not sure, could be useful for composite models and stuff
// TODO output in a size bedrock likes // TODO output in a size bedrock likes
@@ -26,49 +11,7 @@ public class MinecraftGeometryRenderer implements GeometryRenderer {
public static final MinecraftGeometryRenderer INSTANCE = new MinecraftGeometryRenderer(); public static final MinecraftGeometryRenderer INSTANCE = new MinecraftGeometryRenderer();
@Override @Override
public NativeImage render(ItemStack stack) { public TextureHolder render(ResourceLocation location, ItemStack stack) {
TrackingItemStackRenderState itemRenderState = new TrackingItemStackRenderState(); return new RenderedTextureHolder(location, stack);
Minecraft.getInstance().getItemModelResolver().updateForTopItem(itemRenderState, stack, ItemDisplayContext.GUI, null, null, 0);
itemRenderState.setOversizedInGui(true);
GuiItemRenderState guiItemRenderState = new GuiItemRenderState("geometry_render", new Matrix3x2fStack(16), itemRenderState, 0, 0, null);
ScreenRectangle sizeBounds = guiItemRenderState.oversizedItemBounds();
Objects.requireNonNull(sizeBounds);
OversizedItemRenderState oversizedRenderState = new OversizedItemRenderState(guiItemRenderState, sizeBounds.left(), sizeBounds.top(), sizeBounds.right() + 4, sizeBounds.bottom() + 4);
try (OversizedItemRenderer itemRenderer = new OversizedItemRenderer(Minecraft.getInstance().renderBuffers().bufferSource())) {
//noinspection DataFlowIssue
((PictureInPictureCopyRenderer) itemRenderer).rainbow$allowTextureCopy();
itemRenderer.prepare(oversizedRenderState, new GuiRenderState(), 4);
return writeToImage(((PictureInPictureRendererAccessor) itemRenderer).getTexture());
}
}
// Simplified TextureUtil#writeAsPNG with some modifications to just write to a NativeImage, flip the image and just generate it at full size
private static NativeImage writeToImage(GpuTexture texture) {
RenderSystem.assertOnRenderThread();
int width = texture.getWidth(0);
int height = texture.getHeight(0);
int bufferSize = texture.getFormat().pixelSize() * width * height;
GpuBuffer buffer = RenderSystem.getDevice().createBuffer(() -> "Texture output buffer", GpuBuffer.USAGE_COPY_DST | GpuBuffer.USAGE_MAP_READ, bufferSize);
CommandEncoder commandEncoder = RenderSystem.getDevice().createCommandEncoder();
NativeImage image = new NativeImage(width, height, false);
Runnable writer = () -> {
try (GpuBuffer.MappedView mappedView = commandEncoder.mapBuffer(buffer, true, false)) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int colour = mappedView.data().getInt((x + y * width) * texture.getFormat().pixelSize());
image.setPixelABGR(x, height - y - 1, colour);
}
}
}
buffer.close();
};
commandEncoder.copyTextureToBuffer(texture, buffer, 0, writer, 0);
return image;
} }
} }

View File

@@ -0,0 +1,95 @@
package org.geysermc.rainbow.client.render;
import com.mojang.blaze3d.buffers.GpuBuffer;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.systems.CommandEncoder;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.GpuTexture;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.pip.OversizedItemRenderer;
import net.minecraft.client.gui.render.state.GuiItemRenderState;
import net.minecraft.client.gui.render.state.GuiRenderState;
import net.minecraft.client.gui.render.state.pip.OversizedItemRenderState;
import net.minecraft.client.renderer.item.TrackingItemStackRenderState;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ProblemReporter;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import org.geysermc.rainbow.RainbowIO;
import org.geysermc.rainbow.client.mixin.PictureInPictureRendererAccessor;
import org.geysermc.rainbow.image.NativeImageUtil;
import org.geysermc.rainbow.mapping.AssetResolver;
import org.geysermc.rainbow.mapping.PackSerializer;
import org.geysermc.rainbow.mapping.texture.TextureHolder;
import org.joml.Matrix3x2fStack;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class RenderedTextureHolder extends TextureHolder {
private final ItemStack stackToRender;
public RenderedTextureHolder(ResourceLocation location, ItemStack stackToRender) {
super(location);
this.stackToRender = stackToRender;
}
@Override
public Optional<byte[]> load(AssetResolver assetResolver, ProblemReporter reporter) {
throw new UnsupportedOperationException("Rendered texture does not support loading");
}
@Override
public CompletableFuture<?> save(AssetResolver assetResolver, PackSerializer serializer, Path path, ProblemReporter reporter) {
TrackingItemStackRenderState itemRenderState = new TrackingItemStackRenderState();
Minecraft.getInstance().getItemModelResolver().updateForTopItem(itemRenderState, stackToRender, ItemDisplayContext.GUI, null, null, 0);
itemRenderState.setOversizedInGui(true);
GuiItemRenderState guiItemRenderState = new GuiItemRenderState("geometry_render", new Matrix3x2fStack(16), itemRenderState, 0, 0, null);
ScreenRectangle sizeBounds = guiItemRenderState.oversizedItemBounds();
Objects.requireNonNull(sizeBounds);
OversizedItemRenderState oversizedRenderState = new OversizedItemRenderState(guiItemRenderState, sizeBounds.left(), sizeBounds.top(), sizeBounds.right() + 4, sizeBounds.bottom() + 4);
try (OversizedItemRenderer itemRenderer = new OversizedItemRenderer(Minecraft.getInstance().renderBuffers().bufferSource())) {
//noinspection DataFlowIssue
((PictureInPictureCopyRenderer) itemRenderer).rainbow$allowTextureCopy();
itemRenderer.prepare(oversizedRenderState, new GuiRenderState(), 4);
writeAsPNG(serializer, path, ((PictureInPictureRendererAccessor) itemRenderer).getTexture());
}
return CompletableFuture.completedFuture(null);
}
// Simplified TextureUtil#writeAsPNG with some modifications to flip the image and just generate it at full size
private static void writeAsPNG(PackSerializer serializer, Path path, GpuTexture texture) {
RenderSystem.assertOnRenderThread();
int width = texture.getWidth(0);
int height = texture.getHeight(0);
int bufferSize = texture.getFormat().pixelSize() * width * height;
GpuBuffer buffer = RenderSystem.getDevice().createBuffer(() -> "Texture output buffer", GpuBuffer.USAGE_COPY_DST | GpuBuffer.USAGE_MAP_READ, bufferSize);
CommandEncoder commandEncoder = RenderSystem.getDevice().createCommandEncoder();
Runnable writer = () -> {
try (GpuBuffer.MappedView mappedView = commandEncoder.mapBuffer(buffer, true, false)) {
RainbowIO.safeIO(() -> {
try (NativeImage image = new NativeImage(width, height, false)) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int colour = mappedView.data().getInt((x + y * width) * texture.getFormat().pixelSize());
image.setPixelABGR(x, height - y - 1, colour);
}
}
serializer.saveTexture(NativeImageUtil.writeToByteArray(image), path);
}
});
}
buffer.close();
};
commandEncoder.copyTextureToBuffer(texture, buffer, 0, writer, 0);
}
}

View File

@@ -1,6 +1,7 @@
package org.geysermc.rainbow.datagen; package org.geysermc.rainbow.datagen;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.mojang.serialization.Codec; import com.mojang.serialization.Codec;
import net.fabricmc.fabric.api.client.datagen.v1.provider.FabricModelProvider; import net.fabricmc.fabric.api.client.datagen.v1.provider.FabricModelProvider;
import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
@@ -125,7 +126,7 @@ public abstract class RainbowModelProvider extends FabricModelProvider {
public CompletableFuture<?> saveTexture(byte[] texture, Path path) { public CompletableFuture<?> saveTexture(byte[] texture, Path path) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
try { try {
output.writeIfNeeded(path, texture, HashCode.fromBytes(texture)); output.writeIfNeeded(path, texture, Hashing.sha1().hashBytes(texture));
} catch (IOException exception) { } catch (IOException exception) {
LOGGER.error("Failed to save texture to {}", path, exception); LOGGER.error("Failed to save texture to {}", path, exception);
} }

View File

@@ -2,7 +2,7 @@
"schemaVersion": 1, "schemaVersion": 1,
"id": "rainbow-datagen", "id": "rainbow-datagen",
"version": "${version}", "version": "${version}",
"name": "Rainbow", "name": "Rainbow-datagen",
"description": "Rainbow's datagen module", "description": "Rainbow's datagen module",
"authors": [ "authors": [
"GeyserMC contributors" "GeyserMC contributors"

View File

@@ -128,7 +128,7 @@ public class BedrockItemMapper {
bedrockIdentifier = itemModelLocation; bedrockIdentifier = itemModelLocation;
} }
BedrockGeometryContext geometry = BedrockGeometryContext.create(bedrockIdentifier, context.stack, itemModel, context.packContext); BedrockGeometryContext geometry = BedrockGeometryContext.create(bedrockIdentifier, itemModel, context.stack, context.packContext);
if (context.packContext.reportSuccesses()) { if (context.packContext.reportSuccesses()) {
// Not a problem, but just report to get the model printed in the report file // Not a problem, but just report to get the model printed in the report file
context.report("creating mapping for block model " + itemModelLocation); context.report("creating mapping for block model " + itemModelLocation);

View File

@@ -2,9 +2,55 @@ package org.geysermc.rainbow.mapping;
import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; import org.geysermc.rainbow.mapping.geometry.GeometryRenderer;
import org.geysermc.rainbow.definition.GeyserMappings; import org.geysermc.rainbow.definition.GeyserMappings;
import org.geysermc.rainbow.mapping.geometry.MappedGeometryCache;
import org.geysermc.rainbow.pack.PackPaths; import org.geysermc.rainbow.pack.PackPaths;
import java.util.Optional; import java.util.Optional;
public record PackContext(GeyserMappings mappings, PackPaths paths, BedrockItemConsumer itemConsumer, AssetResolver assetResolver, public final class PackContext {
Optional<GeometryRenderer> geometryRenderer, boolean reportSuccesses) {} private final GeyserMappings mappings;
private final PackPaths paths;
private final BedrockItemConsumer itemConsumer;
private final AssetResolver assetResolver;
private final Optional<GeometryRenderer> geometryRenderer;
private final boolean reportSuccesses;
private final MappedGeometryCache geometryCache = new MappedGeometryCache();
public PackContext(GeyserMappings mappings, PackPaths paths, BedrockItemConsumer itemConsumer, AssetResolver assetResolver,
Optional<GeometryRenderer> geometryRenderer, boolean reportSuccesses) {
this.mappings = mappings;
this.paths = paths;
this.itemConsumer = itemConsumer;
this.assetResolver = assetResolver;
this.geometryRenderer = geometryRenderer;
this.reportSuccesses = reportSuccesses;
}
public GeyserMappings mappings() {
return mappings;
}
public PackPaths paths() {
return paths;
}
public BedrockItemConsumer itemConsumer() {
return itemConsumer;
}
public AssetResolver assetResolver() {
return assetResolver;
}
public Optional<GeometryRenderer> geometryRenderer() {
return geometryRenderer;
}
public boolean reportSuccesses() {
return reportSuccesses;
}
public MappedGeometryCache geometryCache() {
return geometryCache;
}
}

View File

@@ -10,8 +10,8 @@ import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.equipment.Equippable; import net.minecraft.world.item.equipment.Equippable;
import org.geysermc.rainbow.mapping.AssetResolver; import org.geysermc.rainbow.mapping.AssetResolver;
import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext;
import org.geysermc.rainbow.mapping.geometry.StitchedGeometry; import org.geysermc.rainbow.mapping.geometry.MappedGeometry;
import org.geysermc.rainbow.mapping.geometry.TextureHolder; import org.geysermc.rainbow.mapping.texture.TextureHolder;
import org.geysermc.rainbow.pack.attachable.BedrockAttachable; import org.geysermc.rainbow.pack.attachable.BedrockAttachable;
import java.util.List; import java.util.List;
@@ -23,8 +23,8 @@ public class AttachableMapper {
public static AttachableCreator mapItem(AssetResolver assetResolver, BedrockGeometryContext geometryContext, DataComponentPatch components) { public static AttachableCreator mapItem(AssetResolver assetResolver, BedrockGeometryContext geometryContext, DataComponentPatch components) {
// Crazy optional statement // Crazy optional statement
// Unfortunately we can't have both equippables and custom models, so we prefer the latter :( // Unfortunately we can't have both equippables and custom models, so we prefer the latter :(
return (bedrockIdentifier, stitchedGeometry, textureConsumer) -> stitchedGeometry return (bedrockIdentifier, textureConsumer) -> geometryContext.geometry()
.map(stitched -> BedrockAttachable.geometry(bedrockIdentifier, stitched.geometry().definitions().getFirst(), stitched.stitchedTextures().location().getPath())) .map(geometry -> BedrockAttachable.geometry(bedrockIdentifier, geometry))
.or(() -> Optional.ofNullable(components.get(DataComponents.EQUIPPABLE)) .or(() -> Optional.ofNullable(components.get(DataComponents.EQUIPPABLE))
.flatMap(optional -> (Optional<Equippable>) optional) .flatMap(optional -> (Optional<Equippable>) optional)
.flatMap(equippable -> equippable.assetId().flatMap(assetResolver::getEquipmentInfo).map(info -> Pair.of(equippable.slot(), info))) .flatMap(equippable -> equippable.assetId().flatMap(assetResolver::getEquipmentInfo).map(info -> Pair.of(equippable.slot(), info)))
@@ -34,7 +34,7 @@ public class AttachableMapper {
.filter(assetInfo -> !assetInfo.getSecond().isEmpty()) .filter(assetInfo -> !assetInfo.getSecond().isEmpty())
.map(assetInfo -> { .map(assetInfo -> {
ResourceLocation equipmentTexture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst())); ResourceLocation equipmentTexture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst()));
textureConsumer.accept(TextureHolder.createFromResources(equipmentTexture)); textureConsumer.accept(TextureHolder.createBuiltIn(equipmentTexture));
return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), equipmentTexture.getPath()); return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), equipmentTexture.getPath());
})) }))
.map(attachable -> { .map(attachable -> {
@@ -59,6 +59,6 @@ public class AttachableMapper {
@FunctionalInterface @FunctionalInterface
public interface AttachableCreator { public interface AttachableCreator {
Optional<BedrockAttachable> create(ResourceLocation bedrockIdentifier, Optional<StitchedGeometry> geometry, Consumer<TextureHolder> textureConsumer); Optional<BedrockAttachable> create(ResourceLocation bedrockIdentifier, Consumer<TextureHolder> textureConsumer);
} }
} }

View File

@@ -1,6 +1,5 @@
package org.geysermc.rainbow.mapping.geometry; package org.geysermc.rainbow.mapping.geometry;
import com.google.common.base.Suppliers;
import net.minecraft.client.renderer.block.model.TextureSlots; import net.minecraft.client.renderer.block.model.TextureSlots;
import net.minecraft.client.resources.model.Material; import net.minecraft.client.resources.model.Material;
import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.client.resources.model.ResolvedModel;
@@ -10,51 +9,41 @@ import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.mapping.PackContext; import org.geysermc.rainbow.mapping.PackContext;
import org.geysermc.rainbow.mapping.animation.AnimationMapper; import org.geysermc.rainbow.mapping.animation.AnimationMapper;
import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext; import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry; import org.geysermc.rainbow.mapping.texture.TextureHolder;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
public record BedrockGeometryContext(Optional<Supplier<StitchedGeometry>> geometry, public record BedrockGeometryContext(Optional<MappedGeometry> geometry,
Optional<BedrockAnimationContext> animation, TextureHolder icon, Optional<BedrockAnimationContext> animation, TextureHolder icon,
boolean handheld) { boolean handheld) {
private static final List<ResourceLocation> HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace") private static final List<ResourceLocation> HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace")
.map(ResourceLocation::withDefaultNamespace) .map(ResourceLocation::withDefaultNamespace)
.toList(); .toList();
public static BedrockGeometryContext create(ResourceLocation bedrockIdentifier, ItemStack stackToRender, ResolvedModel model, PackContext context) { public static BedrockGeometryContext create(ResourceLocation bedrockIdentifier, ResolvedModel model, ItemStack stackToRender, PackContext context) {
ResolvedModel parentModel = model.parent(); ResolvedModel parentModel = model.parent();
// debugName() returns the resource location of the model as a string // debugName() returns the resource location of the model as a string
boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName())); boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName()));
TextureSlots textures = model.getTopTextureSlots(); TextureSlots textures = model.getTopTextureSlots();
Material layer0Texture = textures.getMaterial("layer0"); Material layer0Texture = textures.getMaterial("layer0");
Optional<Supplier<StitchedGeometry>> geometry; Optional<MappedGeometry> geometry;
Optional<BedrockAnimationContext> animation; Optional<BedrockAnimationContext> animation;
TextureHolder icon; TextureHolder icon;
if (layer0Texture != null) { if (layer0Texture != null) {
geometry = Optional.empty(); geometry = Optional.empty();
animation = Optional.empty(); animation = Optional.empty();
icon = TextureHolder.createFromResources(layer0Texture.texture()); icon = TextureHolder.createBuiltIn(layer0Texture.texture());
} else { } else {
// Unknown model (doesn't use layer0), so we immediately assume the geometry is custom // Unknown model (doesn't use layer0), so we immediately assume the geometry is custom
// This check should probably be done differently (actually check if the model is 2D or 3D) // This check should probably be done differently (actually check if the model is 2D or 3D)
ResourceLocation modelLocation = ResourceLocation.parse(model.debugName()); geometry = Optional.of(context.geometryCache().mapGeometry(bedrockIdentifier, model, stackToRender, context));
String safeIdentifier = Rainbow.safeResourceLocation(bedrockIdentifier); animation = Optional.of(AnimationMapper.mapAnimation(Rainbow.safeResourceLocation(bedrockIdentifier), "bone", model.getTopTransforms()));
icon = geometry.get().icon();
geometry = Optional.of(Suppliers.memoize(() -> {
StitchedTextures stitchedTextures = StitchedTextures.stitchModelTextures(textures, context);
BedrockGeometry mappedGeometry = GeometryMapper.mapGeometry(safeIdentifier, "bone", model, stitchedTextures);
return new StitchedGeometry(mappedGeometry, TextureHolder.createProvided(modelLocation.withSuffix("_stitched"), stitchedTextures.stitched()));
}));
animation = Optional.of(AnimationMapper.mapAnimation(safeIdentifier, "bone", model.getTopTransforms()));
icon = context.geometryRenderer().isPresent() ? TextureHolder.createProvided(modelLocation, () -> context.geometryRenderer().orElseThrow().render(stackToRender))
: TextureHolder.createNonExistent(modelLocation);
} }
return new BedrockGeometryContext(geometry, animation, icon, handheld); return new BedrockGeometryContext(geometry, animation, icon, handheld);

View File

@@ -7,6 +7,7 @@ import net.minecraft.client.renderer.block.model.SimpleUnbakedGeometry;
import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.client.resources.model.UnbakedGeometry; import net.minecraft.client.resources.model.UnbakedGeometry;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import org.geysermc.rainbow.mapping.texture.StitchedTextures;
import org.geysermc.rainbow.mixin.FaceBakeryAccessor; import org.geysermc.rainbow.mixin.FaceBakeryAccessor;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry; import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
import org.joml.Vector2f; import org.joml.Vector2f;

View File

@@ -1,9 +1,10 @@
package org.geysermc.rainbow.mapping.geometry; package org.geysermc.rainbow.mapping.geometry;
import com.mojang.blaze3d.platform.NativeImage; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import org.geysermc.rainbow.mapping.texture.TextureHolder;
public interface GeometryRenderer { public interface GeometryRenderer {
NativeImage render(ItemStack stack); TextureHolder render(ResourceLocation location, ItemStack stack);
} }

View File

@@ -0,0 +1,34 @@
package org.geysermc.rainbow.mapping.geometry;
import org.geysermc.rainbow.mapping.PackSerializer;
import org.geysermc.rainbow.mapping.texture.TextureHolder;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
public interface MappedGeometry {
String identifier();
TextureHolder stitchedTextures();
TextureHolder icon();
CompletableFuture<?> save(PackSerializer serializer, Path geometryDirectory, Function<TextureHolder, CompletableFuture<?>> textureSaver);
default CachedGeometry cachedCopy() {
if (this instanceof CachedGeometry cached) {
return cached;
}
return new CachedGeometry(identifier(), TextureHolder.createCopy(stitchedTextures()), TextureHolder.createCopy(icon()));
}
record CachedGeometry(String identifier, TextureHolder stitchedTextures, TextureHolder icon) implements MappedGeometry {
@Override
public CompletableFuture<?> save(PackSerializer serializer, Path geometryDirectory, Function<TextureHolder, CompletableFuture<?>> textureSaver) {
return CompletableFuture.completedFuture(null);
}
}
}

View File

@@ -0,0 +1,49 @@
package org.geysermc.rainbow.mapping.geometry;
import com.google.common.base.Suppliers;
import net.minecraft.client.renderer.block.model.TextureSlots;
import net.minecraft.client.resources.model.Material;
import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.client.resources.model.UnbakedGeometry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.mapping.PackContext;
import org.geysermc.rainbow.mapping.texture.StitchedTextures;
import org.geysermc.rainbow.mapping.texture.TextureHolder;
import org.geysermc.rainbow.mixin.TextureSlotsAccessor;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
import java.util.HashMap;
import java.util.Map;
public class MappedGeometryCache {
private final Map<GeometryCacheKey, MappedGeometryInstance> cachedGeometry = new HashMap<>();
public MappedGeometry mapGeometry(ResourceLocation bedrockIdentifier, ResolvedModel model, ItemStack stackToRender, PackContext context) {
GeometryCacheKey cacheKey = new GeometryCacheKey(model);
MappedGeometry cached = cachedGeometry.get(cacheKey);
if (cached != null) {
return cached.cachedCopy();
}
ResourceLocation modelLocation = ResourceLocation.parse(model.debugName());
ResourceLocation stitchedTexturesLocation = modelLocation.withSuffix("_stitched");
String safeIdentifier = Rainbow.safeResourceLocation(bedrockIdentifier);
StitchedTextures stitchedTextures = StitchedTextures.stitchModelTextures(model.getTopTextureSlots(), context);
BedrockGeometry geometry = GeometryMapper.mapGeometry(safeIdentifier, "bone", model, stitchedTextures);
TextureHolder icon = context.geometryRenderer().isPresent() ? context.geometryRenderer().orElseThrow().render(modelLocation, stackToRender)
: TextureHolder.createNonExistent(modelLocation);
MappedGeometryInstance instance = new MappedGeometryInstance(geometry, TextureHolder.createCustom(stitchedTexturesLocation, stitchedTextures.stitched()), icon);
cachedGeometry.put(cacheKey, instance);
return instance;
}
private record GeometryCacheKey(UnbakedGeometry geometry, Map<String, Material> textures) {
private GeometryCacheKey(ResolvedModel model) {
this(model.getTopGeometry(), ((TextureSlotsAccessor) model.getTopTextureSlots()).getResolvedValues());
}
}
}

View File

@@ -0,0 +1,25 @@
package org.geysermc.rainbow.mapping.geometry;
import org.geysermc.rainbow.mapping.PackSerializer;
import org.geysermc.rainbow.mapping.texture.TextureHolder;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
public record MappedGeometryInstance(BedrockGeometry geometry, TextureHolder stitchedTextures, TextureHolder icon) implements MappedGeometry {
@Override
public String identifier() {
return geometry.definitions().getFirst().info().identifier();
}
@Override
public CompletableFuture<?> save(PackSerializer serializer, Path geometryDirectory, Function<TextureHolder, CompletableFuture<?>> textureSaver) {
return CompletableFuture.allOf(
geometry.save(serializer, geometryDirectory),
textureSaver.apply(stitchedTextures)
);
}
}

View File

@@ -1,5 +0,0 @@
package org.geysermc.rainbow.mapping.geometry;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
public record StitchedGeometry(BedrockGeometry geometry, TextureHolder stitchedTextures) {}

View File

@@ -1,42 +0,0 @@
package org.geysermc.rainbow.mapping.geometry;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ProblemReporter;
import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.RainbowIO;
import org.geysermc.rainbow.image.NativeImageUtil;
import org.geysermc.rainbow.mapping.AssetResolver;
import java.io.InputStream;
import java.util.Optional;
import java.util.function.Supplier;
public record TextureHolder(ResourceLocation location, Optional<Supplier<NativeImage>> supplier, boolean existsInResources) {
public Optional<byte[]> load(AssetResolver assetResolver, ProblemReporter reporter) {
if (existsInResources) {
return RainbowIO.safeIO(() -> {
try (InputStream texture = assetResolver.openAsset(Rainbow.decorateTextureLocation(location))) {
return texture.readAllBytes();
}
});
} else if (supplier.isPresent()) {
return RainbowIO.safeIO(() -> NativeImageUtil.writeToByteArray(supplier.get().get()));
}
reporter.report(() -> "missing texture for " + location + "; please provide it manually");
return Optional.empty();
}
public static TextureHolder createProvided(ResourceLocation location, Supplier<NativeImage> supplier) {
return new TextureHolder(location, Optional.of(supplier), false);
}
public static TextureHolder createFromResources(ResourceLocation location) {
return new TextureHolder(location, Optional.empty(), true);
}
public static TextureHolder createNonExistent(ResourceLocation location) {
return new TextureHolder(location, Optional.empty(), false);
}
}

View File

@@ -0,0 +1,32 @@
package org.geysermc.rainbow.mapping.texture;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ProblemReporter;
import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.RainbowIO;
import org.geysermc.rainbow.mapping.AssetResolver;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Optional;
public class BuiltInTextureHolder extends TextureHolder {
private final ResourceLocation source;
public BuiltInTextureHolder(ResourceLocation location, ResourceLocation source) {
super(location);
this.source = source;
}
@Override
public Optional<byte[]> load(AssetResolver assetResolver, ProblemReporter reporter) {
return RainbowIO.safeIO(() -> {
try (InputStream texture = assetResolver.openAsset(Rainbow.decorateTextureLocation(source))) {
return texture.readAllBytes();
} catch (FileNotFoundException | NullPointerException exception) {
reportMissing(reporter);
return null;
}
});
}
}

View File

@@ -0,0 +1,27 @@
package org.geysermc.rainbow.mapping.texture;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ProblemReporter;
import org.geysermc.rainbow.mapping.AssetResolver;
import org.geysermc.rainbow.mapping.PackSerializer;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class CopyTextureHolder extends TextureHolder {
public CopyTextureHolder(ResourceLocation location) {
super(location);
}
@Override
public Optional<byte[]> load(AssetResolver assetResolver, ProblemReporter reporter) {
return Optional.empty();
}
@Override
public CompletableFuture<?> save(AssetResolver assetResolver, PackSerializer serializer, Path path, ProblemReporter reporter) {
return CompletableFuture.completedFuture(null);
}
}

View File

@@ -0,0 +1,25 @@
package org.geysermc.rainbow.mapping.texture;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ProblemReporter;
import org.geysermc.rainbow.RainbowIO;
import org.geysermc.rainbow.image.NativeImageUtil;
import org.geysermc.rainbow.mapping.AssetResolver;
import java.util.Optional;
import java.util.function.Supplier;
public class CustomTextureHolder extends TextureHolder {
private final Supplier<NativeImage> supplier;
public CustomTextureHolder(ResourceLocation location, Supplier<NativeImage> supplier) {
super(location);
this.supplier = supplier;
}
@Override
public Optional<byte[]> load(AssetResolver assetResolver, ProblemReporter reporter) {
return RainbowIO.safeIO(() -> NativeImageUtil.writeToByteArray(supplier.get()));
}
}

View File

@@ -0,0 +1,20 @@
package org.geysermc.rainbow.mapping.texture;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ProblemReporter;
import org.geysermc.rainbow.mapping.AssetResolver;
import java.util.Optional;
public class MissingTextureHolder extends TextureHolder {
public MissingTextureHolder(ResourceLocation location) {
super(location);
}
@Override
public Optional<byte[]> load(AssetResolver assetResolver, ProblemReporter reporter) {
reportMissing(reporter);
return Optional.empty();
}
}

View File

@@ -1,4 +1,4 @@
package org.geysermc.rainbow.mapping.geometry; package org.geysermc.rainbow.mapping.texture;
import com.mojang.blaze3d.platform.NativeImage; import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.Util; import net.minecraft.Util;

View File

@@ -0,0 +1,56 @@
package org.geysermc.rainbow.mapping.texture;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ProblemReporter;
import org.geysermc.rainbow.mapping.AssetResolver;
import org.geysermc.rainbow.mapping.PackSerializer;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
public abstract class TextureHolder {
protected final ResourceLocation location;
public TextureHolder(ResourceLocation location) {
this.location = location;
}
public abstract Optional<byte[]> load(AssetResolver assetResolver, ProblemReporter reporter);
public CompletableFuture<?> save(AssetResolver assetResolver, PackSerializer serializer, Path path, ProblemReporter reporter) {
return load(assetResolver, reporter)
.map(bytes -> serializer.saveTexture(bytes, path))
.orElse(CompletableFuture.completedFuture(null));
}
public static TextureHolder createCustom(ResourceLocation location, Supplier<NativeImage> supplier) {
return new CustomTextureHolder(location, supplier);
}
public static TextureHolder createBuiltIn(ResourceLocation location, ResourceLocation source) {
return new BuiltInTextureHolder(location, source);
}
public static TextureHolder createBuiltIn(ResourceLocation location) {
return createBuiltIn(location, location);
}
public static TextureHolder createNonExistent(ResourceLocation location) {
return new MissingTextureHolder(location);
}
public static TextureHolder createCopy(TextureHolder original) {
return new CopyTextureHolder(original.location);
}
public ResourceLocation location() {
return location;
}
protected void reportMissing(ProblemReporter reporter) {
reporter.report(() -> "missing texture for " + location + "; please provide it manually");
}
}

View File

@@ -5,8 +5,7 @@ import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.mapping.PackSerializer; import org.geysermc.rainbow.mapping.PackSerializer;
import org.geysermc.rainbow.mapping.attachable.AttachableMapper; import org.geysermc.rainbow.mapping.attachable.AttachableMapper;
import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext;
import org.geysermc.rainbow.mapping.geometry.StitchedGeometry; import org.geysermc.rainbow.mapping.texture.TextureHolder;
import org.geysermc.rainbow.mapping.geometry.TextureHolder;
import org.geysermc.rainbow.pack.attachable.BedrockAttachable; import org.geysermc.rainbow.pack.attachable.BedrockAttachable;
import java.nio.file.Path; import java.nio.file.Path;
@@ -15,27 +14,20 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
public record BedrockItem(ResourceLocation identifier, String textureName, BedrockGeometryContext geometryContext, AttachableMapper.AttachableCreator attachableCreator) { public record BedrockItem(ResourceLocation identifier, String textureName, BedrockGeometryContext geometryContext, AttachableMapper.AttachableCreator attachableCreator) {
public CompletableFuture<?> save(PackSerializer serializer, Path attachableDirectory, Path geometryDirectory, Path animationDirectory, public CompletableFuture<?> save(PackSerializer serializer, Path attachableDirectory, Path geometryDirectory, Path animationDirectory,
Function<TextureHolder, CompletableFuture<?>> textureSaver) { Function<TextureHolder, CompletableFuture<?>> textureSaver) {
List<TextureHolder> attachableTextures = new ArrayList<>();
Optional<BedrockAttachable> createdAttachable = attachableCreator.create(identifier, attachableTextures::add);
return CompletableFuture.allOf( return CompletableFuture.allOf(
textureSaver.apply(geometryContext.icon()), textureSaver.apply(geometryContext.icon()),
CompletableFuture.supplyAsync(() -> geometryContext.geometry().map(Supplier::get))
.thenCompose(stitchedGeometry -> {
List<TextureHolder> attachableTextures = new ArrayList<>();
Optional<BedrockAttachable> createdAttachable = attachableCreator.create(identifier, stitchedGeometry, attachableTextures::add);
return CompletableFuture.allOf(
createdAttachable.map(attachable -> attachable.save(serializer, attachableDirectory)).orElse(noop()), createdAttachable.map(attachable -> attachable.save(serializer, attachableDirectory)).orElse(noop()),
CompletableFuture.allOf(attachableTextures.stream().map(textureSaver).toArray(CompletableFuture[]::new)), CompletableFuture.allOf(attachableTextures.stream().map(textureSaver).toArray(CompletableFuture[]::new)),
stitchedGeometry.map(StitchedGeometry::geometry).map(geometry -> geometry.save(serializer, geometryDirectory)).orElse(noop()), geometryContext.geometry().map(geometry -> geometry.save(serializer, geometryDirectory, textureSaver)).orElse(noop()),
stitchedGeometry.map(StitchedGeometry::stitchedTextures).map(textureSaver).orElse(noop()),
geometryContext.animation().map(context -> context.animation().save(serializer, animationDirectory, Rainbow.safeResourceLocation(identifier))).orElse(noop()) geometryContext.animation().map(context -> context.animation().save(serializer, animationDirectory, Rainbow.safeResourceLocation(identifier))).orElse(noop())
); );
})
);
} }
private static <T> CompletableFuture<T> noop() { private static <T> CompletableFuture<T> noop() {

View File

@@ -20,7 +20,7 @@ import org.geysermc.rainbow.mapping.PackContext;
import org.geysermc.rainbow.mapping.PackSerializer; import org.geysermc.rainbow.mapping.PackSerializer;
import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; import org.geysermc.rainbow.mapping.geometry.GeometryRenderer;
import org.geysermc.rainbow.definition.GeyserMappings; import org.geysermc.rainbow.definition.GeyserMappings;
import org.geysermc.rainbow.mapping.geometry.TextureHolder; import org.geysermc.rainbow.mapping.texture.TextureHolder;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.nio.file.Path; import java.nio.file.Path;
@@ -127,9 +127,7 @@ public class BedrockPack {
Function<TextureHolder, CompletableFuture<?>> textureSaver = texture -> { Function<TextureHolder, CompletableFuture<?>> textureSaver = texture -> {
ResourceLocation textureLocation = Rainbow.decorateTextureLocation(texture.location()); ResourceLocation textureLocation = Rainbow.decorateTextureLocation(texture.location());
return texture.load(context.assetResolver(), reporter) return texture.save(context.assetResolver(), serializer, paths.packRoot().resolve(textureLocation.getPath()), reporter);
.map(bytes -> serializer.saveTexture(bytes, paths.packRoot().resolve(textureLocation.getPath())))
.orElse(CompletableFuture.completedFuture(null));
}; };
for (BedrockItem item : bedrockItems) { for (BedrockItem item : bedrockItems) {

View File

@@ -12,9 +12,9 @@ import net.minecraft.world.entity.EquipmentSlot;
import org.geysermc.rainbow.PackConstants; import org.geysermc.rainbow.PackConstants;
import org.geysermc.rainbow.Rainbow; import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.mapping.PackSerializer; import org.geysermc.rainbow.mapping.PackSerializer;
import org.geysermc.rainbow.mapping.geometry.MappedGeometry;
import org.geysermc.rainbow.pack.BedrockTextures; import org.geysermc.rainbow.pack.BedrockTextures;
import org.geysermc.rainbow.pack.BedrockVersion; import org.geysermc.rainbow.pack.BedrockVersion;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.nio.file.Path; import java.nio.file.Path;
@@ -62,13 +62,13 @@ public record BedrockAttachable(BedrockVersion formatVersion, AttachableInfo inf
.withRenderController(VanillaRenderControllers.ARMOR); .withRenderController(VanillaRenderControllers.ARMOR);
} }
public static BedrockAttachable.Builder geometry(ResourceLocation identifier, BedrockGeometry.GeometryDefinition geometry, String texture) { public static BedrockAttachable.Builder geometry(ResourceLocation identifier, MappedGeometry geometry) {
return builder(identifier) return builder(identifier)
.withMaterial(DisplaySlot.DEFAULT, VanillaMaterials.ENTITY) .withMaterial(DisplaySlot.DEFAULT, VanillaMaterials.ENTITY)
.withMaterial(DisplaySlot.ENCHANTED, VanillaMaterials.ENTITY_ALPHATEST_GLINT) .withMaterial(DisplaySlot.ENCHANTED, VanillaMaterials.ENTITY_ALPHATEST_GLINT)
.withTexture(DisplaySlot.DEFAULT, texture) .withTexture(DisplaySlot.DEFAULT, geometry.stitchedTextures().location().getPath())
.withTexture(DisplaySlot.ENCHANTED, VanillaTextures.ENCHANTED_ITEM_GLINT) .withTexture(DisplaySlot.ENCHANTED, VanillaTextures.ENCHANTED_ITEM_GLINT)
.withGeometry(DisplaySlot.DEFAULT, geometry.info().identifier()) .withGeometry(DisplaySlot.DEFAULT, geometry.identifier())
.withRenderController(VanillaRenderControllers.ITEM_DEFAULT); .withRenderController(VanillaRenderControllers.ITEM_DEFAULT);
} }