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

Work on improving model generation/stitching of multiple textures

This commit is contained in:
Eclipse
2025-10-15 16:41:02 +00:00
parent 94f73dbd06
commit 0ccc78e827
24 changed files with 438 additions and 130 deletions

View File

@@ -8,20 +8,25 @@ import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.item.equipment.EquipmentAsset; import net.minecraft.world.item.equipment.EquipmentAsset;
import org.geysermc.rainbow.client.accessor.ResolvedModelAccessor; import org.geysermc.rainbow.client.accessor.ResolvedModelAccessor;
import org.geysermc.rainbow.client.mixin.EntityRenderDispatcherAccessor; import org.geysermc.rainbow.client.mixin.EntityRenderDispatcherAccessor;
import org.geysermc.rainbow.mapping.AssetResolver; import org.geysermc.rainbow.mapping.AssetResolver;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional; import java.util.Optional;
public class MinecraftAssetResolver implements AssetResolver { public class MinecraftAssetResolver implements AssetResolver {
private final ModelManager modelManager; private final ModelManager modelManager;
private final EquipmentAssetManager equipmentAssetManager; private final EquipmentAssetManager equipmentAssetManager;
private final ResourceManager resourceManager;
public MinecraftAssetResolver(Minecraft minecraft) { public MinecraftAssetResolver(Minecraft minecraft) {
modelManager = minecraft.getModelManager(); modelManager = minecraft.getModelManager();
equipmentAssetManager = ((EntityRenderDispatcherAccessor) minecraft.getEntityRenderDispatcher()).getEquipmentAssets(); equipmentAssetManager = ((EntityRenderDispatcherAccessor) minecraft.getEntityRenderDispatcher()).getEquipmentAssets();
resourceManager = minecraft.getResourceManager();
} }
@Override @Override
@@ -38,4 +43,9 @@ public class MinecraftAssetResolver implements AssetResolver {
public Optional<EquipmentClientInfo> getEquipmentInfo(ResourceKey<EquipmentAsset> key) { public Optional<EquipmentClientInfo> getEquipmentInfo(ResourceKey<EquipmentAsset> key) {
return Optional.of(equipmentAssetManager.get(key)); return Optional.of(equipmentAssetManager.get(key));
} }
@Override
public InputStream openAsset(ResourceLocation location) throws IOException {
return resourceManager.open(location);
}
} }

View File

@@ -16,7 +16,6 @@ import org.geysermc.rainbow.mapping.PackSerializer;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
@@ -24,11 +23,9 @@ import java.util.concurrent.CompletableFuture;
public class MinecraftPackSerializer implements PackSerializer { public class MinecraftPackSerializer implements PackSerializer {
private final HolderLookup.Provider registries; private final HolderLookup.Provider registries;
private final ResourceManager resourceManager;
public MinecraftPackSerializer(Minecraft minecraft) { public MinecraftPackSerializer(Minecraft minecraft) {
registries = Objects.requireNonNull(minecraft.level).registryAccess(); registries = Objects.requireNonNull(minecraft.level).registryAccess();
resourceManager = minecraft.getResourceManager();
} }
@Override @Override
@@ -44,13 +41,12 @@ public class MinecraftPackSerializer implements PackSerializer {
} }
@Override @Override
public CompletableFuture<?> saveTexture(ResourceLocation texture, Path path) { public CompletableFuture<?> saveTexture(byte[] texture, Path path) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
ResourceLocation texturePath = texture.withPath(p -> "textures/" + p + ".png"); try {
try (InputStream inputTexture = resourceManager.open(texturePath)) {
CodecUtil.ensureDirectoryExists(path.getParent()); CodecUtil.ensureDirectoryExists(path.getParent());
try (OutputStream outputTexture = new FileOutputStream(path.toFile())) { try (OutputStream outputTexture = new FileOutputStream(path.toFile())) {
IOUtils.copy(inputTexture, outputTexture); outputTexture.write(texture);
} }
} catch (IOException exception) { } catch (IOException exception) {
// TODO log // TODO log

View File

@@ -88,8 +88,8 @@ public final class PackManager {
Set<BedrockItem> bedrockItems = pack.getBedrockItems(); Set<BedrockItem> bedrockItems = pack.getBedrockItems();
long attachables = bedrockItems.stream().filter(item -> item.attachable().isPresent()).count(); long attachables = bedrockItems.stream().filter(item -> item.attachable().isPresent()).count();
long geometries = bedrockItems.stream().filter(item -> item.geometry().isPresent()).count(); long geometries = bedrockItems.stream().filter(item -> item.geometry().geometry().isPresent()).count();
long animations = bedrockItems.stream().filter(item -> item.animation().isPresent()).count(); long animations = bedrockItems.stream().filter(item -> item.geometry().animation().isPresent()).count();
return """ return """
-- PACK GENERATION REPORT -- -- PACK GENERATION REPORT --

View File

@@ -1,14 +1,33 @@
package org.geysermc.rainbow.client; package org.geysermc.rainbow.client;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.brigadier.arguments.StringArgumentType;
import net.fabricmc.api.ClientModInitializer; import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry; import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.SpriteContents;
import net.minecraft.client.renderer.texture.SpriteLoader;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.metadata.animation.FrameSize;
import net.minecraft.commands.synchronization.SingletonArgumentInfo; import net.minecraft.commands.synchronization.SingletonArgumentInfo;
import net.minecraft.data.AtlasIds;
import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.Rainbow; import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.client.command.CommandSuggestionsArgumentType; import org.geysermc.rainbow.client.command.CommandSuggestionsArgumentType;
import org.geysermc.rainbow.client.command.PackGeneratorCommand; import org.geysermc.rainbow.client.command.PackGeneratorCommand;
import org.geysermc.rainbow.client.mapper.PackMapper; import org.geysermc.rainbow.client.mapper.PackMapper;
import org.geysermc.rainbow.mixin.SpriteContentsAccessor;
import org.geysermc.rainbow.mixin.SpriteLoaderAccessor;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
public class RainbowClient implements ClientModInitializer { public class RainbowClient implements ClientModInitializer {
@@ -18,10 +37,53 @@ public class RainbowClient implements ClientModInitializer {
// TODO export language overrides // TODO export language overrides
@Override @Override
public void onInitializeClient() { public void onInitializeClient() {
ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> PackGeneratorCommand.register(dispatcher, packManager, packMapper)); ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> {
PackGeneratorCommand.register(dispatcher, packManager, packMapper);
dispatcher.register(
ClientCommandManager.literal("DEBUGTEST")
.then(ClientCommandManager.argument("textures", StringArgumentType.greedyString())
.executes(context -> {
SpriteLoader spriteLoader = new SpriteLoader(AtlasIds.BLOCKS, 1024, 16, 16);
List<SpriteContents> sprites = Arrays.stream(StringArgumentType.getString(context, "textures").split(" "))
.map(ResourceLocation::tryParse)
.map(RainbowClient::readSpriteContents)
.toList();
SpriteLoader.Preparations preparations = ((SpriteLoaderAccessor) spriteLoader).invokeStitch(sprites, 0, Util.backgroundExecutor());
try (NativeImage stitched = stitchTextureAtlas(preparations)) {
stitched.writeToFile(FabricLoader.getInstance().getGameDir().resolve("test.png"));
} catch (IOException exception) {
throw new RuntimeException(exception);
}
return 0;
})
)
);
});
ClientTickEvents.START_CLIENT_TICK.register(packMapper::tick); ClientTickEvents.START_CLIENT_TICK.register(packMapper::tick);
ArgumentTypeRegistry.registerArgumentType(Rainbow.getModdedLocation("command_suggestions"), ArgumentTypeRegistry.registerArgumentType(Rainbow.getModdedLocation("command_suggestions"),
CommandSuggestionsArgumentType.class, SingletonArgumentInfo.contextFree(CommandSuggestionsArgumentType::new)); CommandSuggestionsArgumentType.class, SingletonArgumentInfo.contextFree(CommandSuggestionsArgumentType::new));
} }
private static SpriteContents readSpriteContents(ResourceLocation location) {
try (InputStream textureStream = Minecraft.getInstance().getResourceManager().open(location.withPath(path -> "textures/" + path + ".png"))) {
NativeImage texture = NativeImage.read(textureStream);
return new SpriteContents(location, new FrameSize(texture.getWidth(), texture.getHeight()), texture);
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
private static NativeImage stitchTextureAtlas(SpriteLoader.Preparations preparations) {
NativeImage stitched = new NativeImage(preparations.width(), preparations.height(), false);
for (TextureAtlasSprite sprite : preparations.regions().values()) {
try (SpriteContents contents = sprite.contents()) {
((SpriteContentsAccessor) contents).getOriginalImage().copyRect(stitched, 0, 0,
sprite.getX(), sprite.getY(), contents.width(), contents.height(), false, false);
}
}
return stitched;
}
} }

View File

@@ -14,7 +14,16 @@ public class Rainbow {
return ResourceLocation.fromNamespaceAndPath(MOD_ID, path); return ResourceLocation.fromNamespaceAndPath(MOD_ID, path);
} }
// TODO rename remove file
public static String fileSafeResourceLocation(ResourceLocation location) { public static String fileSafeResourceLocation(ResourceLocation location) {
return location.toString().replace(':', '.').replace('/', '_'); return location.toString().replace(':', '.').replace('/', '_');
} }
public static ResourceLocation decorateResourceLocation(ResourceLocation location, String type, String extension) {
return location.withPath(path -> type + "/" + path + "." + extension);
}
public static ResourceLocation decorateTextureLocation(ResourceLocation location) {
return decorateResourceLocation(location, "textures", "png");
}
} }

View File

@@ -0,0 +1,44 @@
package org.geysermc.rainbow.image;
import com.mojang.blaze3d.platform.NativeImage;
import org.geysermc.rainbow.mixin.NativeImageAccessor;
import org.lwjgl.stb.STBImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Pipe;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
public class NativeImageUtil {
// Adjusted NativeImage#writeToFile
@SuppressWarnings("DataFlowIssue")
public static byte[] writeToByteArray(NativeImage image) throws IOException {
if (!image.format().supportedByStb()) {
throw new UnsupportedOperationException("Don't know how to write format " + image.format());
} else {
((NativeImageAccessor) (Object) image).invokeCheckAllocated();
Pipe pipe = Pipe.open();
try (WritableByteChannel outputChannel = pipe.sink()) {
if (!((NativeImageAccessor) (Object) image).invokeWriteToChannel(outputChannel)) {
throw new IOException("Could not write image to pipe: " + STBImage.stbi_failure_reason());
}
}
try (ReadableByteChannel inputChannel = pipe.source()) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ByteBuffer buffer = ByteBuffer.allocate(4096);
while (inputChannel.read(buffer) != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
bytes.write(buffer.get());
}
buffer.clear();
}
return bytes.toByteArray();
}
}
}
}

View File

@@ -7,6 +7,8 @@ import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.equipment.EquipmentAsset; import net.minecraft.world.item.equipment.EquipmentAsset;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional; import java.util.Optional;
public interface AssetResolver { public interface AssetResolver {
@@ -16,4 +18,6 @@ public interface AssetResolver {
Optional<ClientItem> getClientItem(ResourceLocation location); Optional<ClientItem> getClientItem(ResourceLocation location);
Optional<EquipmentClientInfo> getEquipmentInfo(ResourceKey<EquipmentAsset> key); Optional<EquipmentClientInfo> getEquipmentInfo(ResourceKey<EquipmentAsset> key);
InputStream openAsset(ResourceLocation location) throws IOException;
} }

View File

@@ -21,8 +21,6 @@ import net.minecraft.client.renderer.item.properties.select.Charge;
import net.minecraft.client.renderer.item.properties.select.ContextDimension; import net.minecraft.client.renderer.item.properties.select.ContextDimension;
import net.minecraft.client.renderer.item.properties.select.DisplayContext; import net.minecraft.client.renderer.item.properties.select.DisplayContext;
import net.minecraft.client.renderer.item.properties.select.TrimMaterialProperty; import net.minecraft.client.renderer.item.properties.select.TrimMaterialProperty;
import net.minecraft.client.resources.model.Material;
import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.core.component.DataComponents; import net.minecraft.core.component.DataComponents;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@@ -37,11 +35,8 @@ import net.minecraft.world.item.component.ItemAttributeModifiers;
import net.minecraft.world.item.equipment.trim.TrimMaterial; import net.minecraft.world.item.equipment.trim.TrimMaterial;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.geysermc.rainbow.mapping.animation.AnimationMapper;
import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext;
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.GeometryMapper;
import org.geysermc.rainbow.definition.GeyserBaseDefinition; import org.geysermc.rainbow.definition.GeyserBaseDefinition;
import org.geysermc.rainbow.definition.GeyserItemDefinition; import org.geysermc.rainbow.definition.GeyserItemDefinition;
import org.geysermc.rainbow.definition.GeyserLegacyDefinition; import org.geysermc.rainbow.definition.GeyserLegacyDefinition;
@@ -52,9 +47,7 @@ import org.geysermc.rainbow.definition.predicate.GeyserPredicate;
import org.geysermc.rainbow.definition.predicate.GeyserRangeDispatchPredicate; import org.geysermc.rainbow.definition.predicate.GeyserRangeDispatchPredicate;
import org.geysermc.rainbow.mixin.LateBoundIdMapperAccessor; import org.geysermc.rainbow.mixin.LateBoundIdMapperAccessor;
import org.geysermc.rainbow.mixin.RangeSelectItemModelAccessor; import org.geysermc.rainbow.mixin.RangeSelectItemModelAccessor;
import org.geysermc.rainbow.mixin.TextureSlotsAccessor;
import org.geysermc.rainbow.pack.BedrockItem; import org.geysermc.rainbow.pack.BedrockItem;
import org.geysermc.rainbow.pack.BedrockTextures;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -62,9 +55,6 @@ import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
public class BedrockItemMapper { public class BedrockItemMapper {
private static final List<ResourceLocation> HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace")
.map(ResourceLocation::withDefaultNamespace)
.toList();
private static final List<ResourceLocation> TRIMMABLE_ARMOR_TAGS = Stream.of("is_armor", "trimmable_armors") private static final List<ResourceLocation> TRIMMABLE_ARMOR_TAGS = Stream.of("is_armor", "trimmable_armors")
.map(ResourceLocation::withDefaultNamespace) .map(ResourceLocation::withDefaultNamespace)
.toList(); .toList();
@@ -117,7 +107,7 @@ public class BedrockItemMapper {
case ConditionalItemModel.Unbaked conditional -> mapConditionalModel(conditional, context.child("condition model ")); case ConditionalItemModel.Unbaked conditional -> mapConditionalModel(conditional, context.child("condition model "));
case RangeSelectItemModel.Unbaked rangeSelect -> mapRangeSelectModel(rangeSelect, context.child("range select model ")); case RangeSelectItemModel.Unbaked rangeSelect -> mapRangeSelectModel(rangeSelect, context.child("range select model "));
case SelectItemModel.Unbaked select -> mapSelectModel(select, context.child("select model ")); case SelectItemModel.Unbaked select -> mapSelectModel(select, context.child("select model "));
default -> context.reporter.report(() -> "unsupported item model " + getModelId(model)); default -> context.report("unsupported item model " + getModelId(model));
} }
} }
@@ -126,10 +116,6 @@ public class BedrockItemMapper {
context.packContext().assetResolver().getResolvedModel(itemModelLocation) context.packContext().assetResolver().getResolvedModel(itemModelLocation)
.ifPresentOrElse(itemModel -> { .ifPresentOrElse(itemModel -> {
ResolvedModel parentModel = itemModel.parent();
// debugName() returns the resource location of the model as a string
boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName()));
ResourceLocation bedrockIdentifier; ResourceLocation bedrockIdentifier;
if (itemModelLocation.getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) { if (itemModelLocation.getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) {
bedrockIdentifier = ResourceLocation.fromNamespaceAndPath("geyser_mc", itemModelLocation.getPath()); bedrockIdentifier = ResourceLocation.fromNamespaceAndPath("geyser_mc", itemModelLocation.getPath());
@@ -137,29 +123,13 @@ public class BedrockItemMapper {
bedrockIdentifier = itemModelLocation; bedrockIdentifier = itemModelLocation;
} }
Material layer0Texture = itemModel.getTopTextureSlots().getMaterial("layer0"); BedrockGeometryContext geometry = BedrockGeometryContext.create(itemModel);
Optional<ResourceLocation> texture; if (context.packContext.reportSuccesses()) {
Optional<ResolvedModel> customGeometry;
if (layer0Texture != null) {
texture = Optional.of(layer0Texture.texture());
customGeometry = Optional.empty();
} else {
// We can't stitch multiple textures together yet, so we just grab the first one we see
// This will only work properly for models with just one texture
texture = ((TextureSlotsAccessor) itemModel.getTopTextureSlots()).getResolvedValues().values().stream()
.map(Material::texture)
.findAny();
// Unknown texture (doesn't use layer0), so we immediately assume the geometry is custom
// This check should probably be done differently
customGeometry = Optional.of(itemModel);
}
texture.ifPresentOrElse(itemTexture -> {
// 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.reporter.report(() -> "creating mapping for block model " + itemModelLocation); context.report("creating mapping for block model " + itemModelLocation);
context.create(bedrockIdentifier, itemTexture, handheld, customGeometry); }
}, () -> context.reporter.report(() -> "not mapping block model " + itemModelLocation + " because it has no texture")); context.create(bedrockIdentifier, geometry);
}, () -> context.reporter.report(() -> "missing block model " + itemModelLocation)); }, () -> context.report("missing block model " + itemModelLocation));
} }
private static void mapConditionalModel(ConditionalItemModel.Unbaked model, MappingContext context) { private static void mapConditionalModel(ConditionalItemModel.Unbaked model, MappingContext context) {
@@ -176,7 +146,7 @@ public class BedrockItemMapper {
ItemModel.Unbaked onFalse = model.onFalse(); ItemModel.Unbaked onFalse = model.onFalse();
if (predicateProperty == null) { if (predicateProperty == null) {
context.reporter.report(() -> "unsupported conditional model property " + property + ", only mapping on_false"); context.report("unsupported conditional model property " + property + ", only mapping on_false");
mapItem(onFalse, context.child("condition on_false (unsupported property)")); mapItem(onFalse, context.child("condition on_false (unsupported property)"));
return; return;
} }
@@ -197,7 +167,7 @@ public class BedrockItemMapper {
}; };
if (predicateProperty == null) { if (predicateProperty == null) {
context.reporter.report(() -> "unsupported range dispatch model property " + property + ", only mapping fallback, if it is present"); context.report("unsupported range dispatch model property " + property + ", only mapping fallback, if it is present");
} else { } else {
for (RangeSelectItemModel.Entry entry : model.entries()) { for (RangeSelectItemModel.Entry entry : model.entries()) {
mapItem(entry.model(), context.with(new GeyserRangeDispatchPredicate(predicateProperty, entry.threshold(), model.scale()), "threshold " + entry.threshold())); mapItem(entry.model(), context.with(new GeyserRangeDispatchPredicate(predicateProperty, entry.threshold(), model.scale()), "threshold " + entry.threshold()));
@@ -223,7 +193,7 @@ public class BedrockItemMapper {
if (dataConstructor == null) { if (dataConstructor == null) {
if (unbakedSwitch.property() instanceof DisplayContext) { if (unbakedSwitch.property() instanceof DisplayContext) {
context.reporter.report(() -> "unsupported select model property display_context, only mapping \"gui\" case, if it exists"); context.report("unsupported select model property display_context, only mapping \"gui\" case, if it exists");
for (SelectItemModel.SwitchCase<?> switchCase : cases) { for (SelectItemModel.SwitchCase<?> switchCase : cases) {
if (switchCase.values().contains(ItemDisplayContext.GUI)) { if (switchCase.values().contains(ItemDisplayContext.GUI)) {
mapItem(switchCase.model(), context.child("select GUI display_context case (unsupported property) ")); mapItem(switchCase.model(), context.child("select GUI display_context case (unsupported property) "));
@@ -231,7 +201,7 @@ public class BedrockItemMapper {
} }
} }
} }
context.reporter.report(() -> "unsupported select model property " + unbakedSwitch.property() + ", only mapping fallback, if present"); context.report("unsupported select model property " + unbakedSwitch.property() + ", only mapping fallback, if present");
model.fallback().ifPresent(fallback -> mapItem(fallback, context.child("select fallback case (unsupported property) "))); model.fallback().ifPresent(fallback -> mapItem(fallback, context.child("select fallback case (unsupported property) ")));
return; return;
} }
@@ -255,17 +225,11 @@ public class BedrockItemMapper {
return new MappingContext(predicateStack, stack, reporter.forChild(() -> childName), definitionCreator, packContext); return new MappingContext(predicateStack, stack, reporter.forChild(() -> childName), definitionCreator, packContext);
} }
public void create(ResourceLocation bedrockIdentifier, ResourceLocation texture, boolean displayHandheld, public void create(ResourceLocation bedrockIdentifier, BedrockGeometryContext geometry) {
Optional<ResolvedModel> customModel) { List<ResourceLocation> tags = stack.is(ItemTags.TRIMMABLE_ARMOR) ? TRIMMABLE_ARMOR_TAGS : List.of();
List<ResourceLocation> tags;
if (stack.is(ItemTags.TRIMMABLE_ARMOR)) {
tags = TRIMMABLE_ARMOR_TAGS;
} else {
tags = List.of();
}
GeyserBaseDefinition base = new GeyserBaseDefinition(bedrockIdentifier, Optional.ofNullable(stack.getHoverName().tryCollapseToString()), predicateStack, GeyserBaseDefinition base = new GeyserBaseDefinition(bedrockIdentifier, Optional.ofNullable(stack.getHoverName().tryCollapseToString()), predicateStack,
new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, displayHandheld, calculateProtectionValue(stack), tags), new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, geometry.handheld(), calculateProtectionValue(stack), tags),
stack.getComponentsPatch()); stack.getComponentsPatch());
try { try {
packContext.mappings().map(stack.getItemHolder(), definitionCreator.apply(base)); packContext.mappings().map(stack.getItemHolder(), definitionCreator.apply(base));
@@ -274,26 +238,13 @@ public class BedrockItemMapper {
return; return;
} }
// TODO Should probably get a better way to get geometry texture // TODO move attachable mapping somewhere else for cleaner code?
String safeIdentifier = base.textureName(); packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), geometry,
String bone = "bone"; AttachableMapper.mapItem(stack.getComponentsPatch(), bedrockIdentifier, geometry, packContext.assetResolver(), packContext.additionalTextureConsumer())));
ResourceLocation geometryTexture = texture;
Optional<BedrockGeometryContext> bedrockGeometry = customModel.flatMap(model -> GeometryMapper.mapGeometry(safeIdentifier, bone, model, geometryTexture));
Optional<BedrockAnimationContext> bedrockAnimation = customModel.map(model -> AnimationMapper.mapAnimation(safeIdentifier, bone, model.getTopTransforms()));
boolean exportTexture = true;
if (customModel.isPresent()) {
texture = texture.withPath(path -> path + "_icon");
// FIXME Bit of a hack, preferably render geometry at a later stage
exportTexture = !packContext.geometryRenderer().render(stack, packContext.paths().packRoot().resolve(BedrockTextures.TEXTURES_FOLDER + texture.getPath() + ".png"));
packContext.additionalTextureConsumer().accept(geometryTexture);
} }
packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), texture, exportTexture, public void report(String problem) {
AttachableMapper.mapItem(stack.getComponentsPatch(), bedrockIdentifier, bedrockGeometry, bedrockAnimation, reporter.report(() -> problem);
packContext.assetResolver(),
packContext.additionalTextureConsumer()),
bedrockGeometry.map(BedrockGeometryContext::geometry), bedrockAnimation.map(BedrockAnimationContext::animation)));
} }
private static int calculateProtectionValue(ItemStack stack) { private static int calculateProtectionValue(ItemStack stack) {

View File

@@ -1,13 +1,13 @@
package org.geysermc.rainbow.mapping; package org.geysermc.rainbow.mapping;
import net.minecraft.resources.ResourceLocation;
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.pack.PackPaths; import org.geysermc.rainbow.pack.PackPaths;
import java.util.function.Consumer; import java.util.function.Consumer;
public record PackContext(GeyserMappings mappings, PackPaths paths, BedrockItemConsumer itemConsumer, AssetResolver assetResolver, public record PackContext(GeyserMappings mappings, PackPaths paths, BedrockItemConsumer itemConsumer, AssetResolver assetResolver,
GeometryRenderer geometryRenderer, Consumer<ResourceLocation> additionalTextureConsumer, GeometryRenderer geometryRenderer, Consumer<TextureHolder> additionalTextureConsumer,
boolean reportSuccesses) { boolean reportSuccesses) {
} }

View File

@@ -1,7 +1,6 @@
package org.geysermc.rainbow.mapping; package org.geysermc.rainbow.mapping;
import com.mojang.serialization.Codec; import com.mojang.serialization.Codec;
import net.minecraft.resources.ResourceLocation;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -10,5 +9,5 @@ public interface PackSerializer {
<T> CompletableFuture<?> saveJson(Codec<T> codec, T object, Path path); <T> CompletableFuture<?> saveJson(Codec<T> codec, T object, Path path);
CompletableFuture<?> saveTexture(ResourceLocation texture, Path path); CompletableFuture<?> saveTexture(byte[] texture, Path path);
} }

View File

@@ -9,8 +9,8 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.EquipmentSlot; 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.animation.BedrockAnimationContext;
import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext;
import org.geysermc.rainbow.mapping.geometry.TextureHolder;
import org.geysermc.rainbow.pack.attachable.BedrockAttachable; import org.geysermc.rainbow.pack.attachable.BedrockAttachable;
import java.util.List; import java.util.List;
@@ -19,12 +19,12 @@ import java.util.function.Consumer;
public class AttachableMapper { public class AttachableMapper {
public static Optional<BedrockAttachable> mapItem(DataComponentPatch components, ResourceLocation bedrockIdentifier, Optional<BedrockGeometryContext> customGeometry, public static Optional<BedrockAttachable> mapItem(DataComponentPatch components, ResourceLocation bedrockIdentifier, BedrockGeometryContext geometryContext,
Optional<BedrockAnimationContext> customAnimation, AssetResolver assetResolver, Consumer<ResourceLocation> textureConsumer) { AssetResolver assetResolver, Consumer<TextureHolder> textureConsumer) {
// 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 customGeometry return geometryContext.geometry()
.map(geometry -> BedrockAttachable.geometry(bedrockIdentifier, geometry.geometry().definitions().getFirst(), geometry.texture().getPath())) .map(geometry -> BedrockAttachable.geometry(bedrockIdentifier, geometry.definitions().getFirst(), geometryContext.texture().location().getPath()))
.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)))
@@ -33,14 +33,14 @@ public class AttachableMapper {
.mapSecond(info -> info.getLayers(getLayer(assetInfo.getFirst())))) .mapSecond(info -> info.getLayers(getLayer(assetInfo.getFirst()))))
.filter(assetInfo -> !assetInfo.getSecond().isEmpty()) .filter(assetInfo -> !assetInfo.getSecond().isEmpty())
.map(assetInfo -> { .map(assetInfo -> {
ResourceLocation texture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst())); ResourceLocation equipmentTexture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst()));
textureConsumer.accept(texture); textureConsumer.accept(new TextureHolder(equipmentTexture));
return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), texture.getPath()); return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), equipmentTexture.getPath());
})) }))
.map(attachable -> { .map(attachable -> {
customAnimation.ifPresent(context -> { geometryContext.animation().ifPresent(animation -> {
attachable.withAnimation("first_person", context.firstPerson()); attachable.withAnimation("first_person", animation.firstPerson());
attachable.withAnimation("third_person", context.thirdPerson()); attachable.withAnimation("third_person", animation.thirdPerson());
attachable.withScript("animate", "first_person", "context.is_first_person == 1.0"); attachable.withScript("animate", "first_person", "context.is_first_person == 1.0");
attachable.withScript("animate", "third_person", "context.is_first_person == 0.0"); attachable.withScript("animate", "third_person", "context.is_first_person == 0.0");
}); });

View File

@@ -1,6 +1,53 @@
package org.geysermc.rainbow.mapping.geometry; package org.geysermc.rainbow.mapping.geometry;
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.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.mapping.animation.AnimationMapper;
import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry; import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
public record BedrockGeometryContext(BedrockGeometry geometry, ResourceLocation texture) {} import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public record BedrockGeometryContext(Optional<BedrockGeometry> geometry, Optional<BedrockAnimationContext> animation, TextureHolder texture,
boolean handheld) {
private static final List<ResourceLocation> HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace")
.map(ResourceLocation::withDefaultNamespace)
.toList();
public static BedrockGeometryContext create(ResolvedModel model) {
ResolvedModel parentModel = model.parent();
// debugName() returns the resource location of the model as a string
boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName()));
TextureSlots textures = model.getTopTextureSlots();
Material layer0Texture = textures.getMaterial("layer0");
Optional<BedrockGeometry> geometry;
Optional<BedrockAnimationContext> animation;
TextureHolder texture;
if (layer0Texture != null) {
geometry = Optional.empty();
animation = Optional.empty();
texture = new TextureHolder(layer0Texture.texture());
} else {
// 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)
ResourceLocation modelLocation = ResourceLocation.parse(model.debugName());
String safeIdentifier = Rainbow.fileSafeResourceLocation(modelLocation);
StitchedTextures stitchedTextures = StitchedTextures.stitchModelTextures(textures);
geometry = GeometryMapper.mapGeometry(safeIdentifier, "bone", model, stitchedTextures);
animation = Optional.of(AnimationMapper.mapAnimation(safeIdentifier, "bone", model.getTopTransforms()));
texture = new TextureHolder(modelLocation.withSuffix("_stitched"), Optional.of(stitchedTextures.stitched()));
// TODO geometry rendering
}
return new BedrockGeometryContext(geometry, animation, texture, handheld);
}
}

View File

@@ -7,7 +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 net.minecraft.resources.ResourceLocation; 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;
import org.joml.Vector3f; import org.joml.Vector3f;
@@ -19,7 +19,7 @@ import java.util.Optional;
public class GeometryMapper { public class GeometryMapper {
private static final Vector3fc CENTRE_OFFSET = new Vector3f(8.0F, 0.0F, 8.0F); private static final Vector3fc CENTRE_OFFSET = new Vector3f(8.0F, 0.0F, 8.0F);
public static Optional<BedrockGeometryContext> mapGeometry(String identifier, String boneName, ResolvedModel model, ResourceLocation texture) { public static Optional<BedrockGeometry> mapGeometry(String identifier, String boneName, ResolvedModel model, StitchedTextures textures) {
UnbakedGeometry top = model.getTopGeometry(); UnbakedGeometry top = model.getTopGeometry();
if (top == UnbakedGeometry.EMPTY) { if (top == UnbakedGeometry.EMPTY) {
return Optional.empty(); return Optional.empty();
@@ -31,9 +31,8 @@ public class GeometryMapper {
builder.withVisibleBoundsHeight(4.0F); builder.withVisibleBoundsHeight(4.0F);
builder.withVisibleBoundsOffset(new Vector3f(0.0F, 0.75F, 0.0F)); builder.withVisibleBoundsOffset(new Vector3f(0.0F, 0.75F, 0.0F));
// TODO proper texture size builder.withTextureWidth(textures.width());
builder.withTextureWidth(16); builder.withTextureHeight(textures.height());
builder.withTextureHeight(16);
BedrockGeometry.Bone.Builder bone = BedrockGeometry.bone(boneName); BedrockGeometry.Bone.Builder bone = BedrockGeometry.bone(boneName);
@@ -43,7 +42,7 @@ public class GeometryMapper {
SimpleUnbakedGeometry geometry = (SimpleUnbakedGeometry) top; SimpleUnbakedGeometry geometry = (SimpleUnbakedGeometry) top;
for (BlockElement element : geometry.elements()) { for (BlockElement element : geometry.elements()) {
// TODO the origin here is wrong, some models seem to be mirrored weirdly in blockbench // TODO the origin here is wrong, some models seem to be mirrored weirdly in blockbench
BedrockGeometry.Cube cube = mapBlockElement(element).build(); BedrockGeometry.Cube cube = mapBlockElement(element, textures).build();
bone.withCube(cube); bone.withCube(cube);
min.min(cube.origin()); min.min(cube.origin());
max.max(cube.origin().add(cube.size(), new Vector3f())); max.max(cube.origin().add(cube.size(), new Vector3f()));
@@ -55,23 +54,26 @@ public class GeometryMapper {
// Bind to the bone of the current item slot // Bind to the bone of the current item slot
bone.withBinding("q.item_slot_to_bone_name(context.item_slot)"); bone.withBinding("q.item_slot_to_bone_name(context.item_slot)");
return Optional.of(new BedrockGeometryContext(builder.withBone(bone).build(), texture)); return Optional.of(builder.withBone(bone).build());
} }
private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element) { private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element, StitchedTextures textures) {
// The centre of the model is back by 8 in the X and Z direction on Java, so move the origin of the cube and the pivot like that // The centre of the model is back by 8 in the X and Z direction on Java, so move the origin of the cube and the pivot like that
BedrockGeometry.Cube.Builder builder = BedrockGeometry.cube(element.from().sub(CENTRE_OFFSET, new Vector3f()), element.to().sub(element.from(), new Vector3f())); BedrockGeometry.Cube.Builder builder = BedrockGeometry.cube(element.from().sub(CENTRE_OFFSET, new Vector3f()), element.to().sub(element.from(), new Vector3f()));
for (Map.Entry<Direction, BlockElementFace> faceEntry : element.faces().entrySet()) { for (Map.Entry<Direction, BlockElementFace> faceEntry : element.faces().entrySet()) {
// TODO texture key
Direction direction = faceEntry.getKey(); Direction direction = faceEntry.getKey();
BlockElementFace face = faceEntry.getValue(); BlockElementFace face = faceEntry.getValue();
Vector2f uvOrigin; Vector2f uvOrigin;
Vector2f uvSize; Vector2f uvSize;
BlockElementFace.UVs uvs = face.uvs(); BlockElementFace.UVs uvs = face.uvs();
if (uvs != null) { if (uvs == null) {
// Up and down faces are special // Java defaults to a set of UV values determined by the position of the face if no UV values were specified
uvs = FaceBakeryAccessor.invokeDefaultFaceUV(element.from(), element.to(), direction);
}
// Up and down faces are special and have their UVs flipped
if (direction.getAxis() == Direction.Axis.Y) { if (direction.getAxis() == Direction.Axis.Y) {
uvOrigin = new Vector2f(uvs.maxU(), uvs.maxV()); uvOrigin = new Vector2f(uvs.maxU(), uvs.maxV());
uvSize = new Vector2f(uvs.minU() - uvs.maxU(), uvs.minV() - uvs.maxV()); uvSize = new Vector2f(uvs.minU() - uvs.maxU(), uvs.minV() - uvs.maxV());
@@ -79,11 +81,9 @@ public class GeometryMapper {
uvOrigin = new Vector2f(uvs.minU(), uvs.minV()); uvOrigin = new Vector2f(uvs.minU(), uvs.minV());
uvSize = new Vector2f(uvs.maxU() - uvs.minU(), uvs.maxV() - uvs.minV()); uvSize = new Vector2f(uvs.maxU() - uvs.minU(), uvs.maxV() - uvs.minV());
} }
} else {
uvOrigin = new Vector2f();
uvSize = new Vector2f();
}
// If the texture was stitched (which it should have been, unless it doesn't exist), offset the UVs by the texture's starting UV
textures.getSprite(face.texture()).ifPresent(sprite -> uvOrigin.add(sprite.getX(), sprite.getY()));
builder.withFace(direction, uvOrigin, uvSize, face.rotation()); builder.withFace(direction, uvOrigin, uvSize, face.rotation());
} }

View File

@@ -0,0 +1,77 @@
package org.geysermc.rainbow.mapping.geometry;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.block.model.TextureSlots;
import net.minecraft.client.renderer.texture.SpriteContents;
import net.minecraft.client.renderer.texture.SpriteLoader;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.metadata.animation.FrameSize;
import net.minecraft.client.resources.model.Material;
import net.minecraft.data.AtlasIds;
import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.mixin.SpriteContentsAccessor;
import org.geysermc.rainbow.mixin.SpriteLoaderAccessor;
import org.geysermc.rainbow.mixin.TextureSlotsAccessor;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
public record StitchedTextures(Map<String, TextureAtlasSprite> sprites, Supplier<NativeImage> stitched, int width, int height) {
public Optional<TextureAtlasSprite> getSprite(String key) {
if (TextureSlotsAccessor.invokeIsTextureReference(key)) {
key = key.substring(1);
}
return Optional.ofNullable(sprites.get(key));
}
public static StitchedTextures stitchModelTextures(TextureSlots textures) {
Map<String, Material> materials = ((TextureSlotsAccessor) textures).getResolvedValues();
SpriteLoader.Preparations preparations = prepareStitching(materials.values().stream().map(Material::texture));
Map<String, TextureAtlasSprite> sprites = new HashMap<>();
for (Map.Entry<String, Material> material : materials.entrySet()) {
sprites.put(material.getKey(), preparations.getSprite(material.getValue().texture()));
}
return new StitchedTextures(Map.copyOf(sprites), () -> stitchTextureAtlas(preparations), preparations.width(), preparations.height());
}
private static SpriteLoader.Preparations prepareStitching(Stream<ResourceLocation> textures) {
// Atlas ID doesn't matter much here, but BLOCKS is the most appropriate
// Not sure if 1024 should be the max supported texture size, but it seems to work
SpriteLoader spriteLoader = new SpriteLoader(AtlasIds.BLOCKS, 1024, 16, 16);
List<SpriteContents> sprites = textures.map(StitchedTextures::readSpriteContents).toList();
return ((SpriteLoaderAccessor) spriteLoader).invokeStitch(sprites, 0, Util.backgroundExecutor());
}
private static SpriteContents readSpriteContents(ResourceLocation location) {
// TODO decorate path util
// TODO don't use ResourceManager
// TODO IO is on main thread here?
try (InputStream textureStream = Minecraft.getInstance().getResourceManager().open(location.withPath(path -> "textures/" + path + ".png"))) {
NativeImage texture = NativeImage.read(textureStream);
return new SpriteContents(location, new FrameSize(texture.getWidth(), texture.getHeight()), texture);
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
private static NativeImage stitchTextureAtlas(SpriteLoader.Preparations preparations) {
NativeImage stitched = new NativeImage(preparations.width(), preparations.height(), false);
for (TextureAtlasSprite sprite : preparations.regions().values()) {
try (SpriteContents contents = sprite.contents()) {
((SpriteContentsAccessor) contents).getOriginalImage().copyRect(stitched, 0, 0,
sprite.getX(), sprite.getY(), contents.width(), contents.height(), false, false);
}
}
return stitched;
}
}

View File

@@ -0,0 +1,14 @@
package org.geysermc.rainbow.mapping.geometry;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.resources.ResourceLocation;
import java.util.Optional;
import java.util.function.Supplier;
public record TextureHolder(ResourceLocation location, Optional<Supplier<NativeImage>> supplier) {
public TextureHolder(ResourceLocation location) {
this(location, Optional.empty());
}
}

View File

@@ -0,0 +1,17 @@
package org.geysermc.rainbow.mixin;
import net.minecraft.client.renderer.block.model.BlockElementFace;
import net.minecraft.client.renderer.block.model.FaceBakery;
import net.minecraft.core.Direction;
import org.joml.Vector3fc;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(FaceBakery.class)
public interface FaceBakeryAccessor {
@Invoker
static BlockElementFace.UVs invokeDefaultFaceUV(Vector3fc posFrom, Vector3fc posTo, Direction facing) {
throw new AssertionError();
}
}

View File

@@ -0,0 +1,18 @@
package org.geysermc.rainbow.mixin;
import com.mojang.blaze3d.platform.NativeImage;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
@Mixin(NativeImage.class)
public interface NativeImageAccessor {
@Invoker
void invokeCheckAllocated();
@Invoker
boolean invokeWriteToChannel(WritableByteChannel channel) throws IOException;
}

View File

@@ -0,0 +1,13 @@
package org.geysermc.rainbow.mixin;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.client.renderer.texture.SpriteContents;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(SpriteContents.class)
public interface SpriteContentsAccessor {
@Accessor
NativeImage getOriginalImage();
}

View File

@@ -0,0 +1,16 @@
package org.geysermc.rainbow.mixin;
import net.minecraft.client.renderer.texture.SpriteContents;
import net.minecraft.client.renderer.texture.SpriteLoader;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
import java.util.List;
import java.util.concurrent.Executor;
@Mixin(SpriteLoader.class)
public interface SpriteLoaderAccessor {
@Invoker
SpriteLoader.Preparations invokeStitch(List<SpriteContents> contents, int mipLevel, Executor executor);
}

View File

@@ -4,6 +4,7 @@ import net.minecraft.client.renderer.block.model.TextureSlots;
import net.minecraft.client.resources.model.Material; import net.minecraft.client.resources.model.Material;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor; import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import java.util.Map; import java.util.Map;
@@ -12,4 +13,9 @@ public interface TextureSlotsAccessor {
@Accessor @Accessor
Map<String, Material> getResolvedValues(); Map<String, Material> getResolvedValues();
@Invoker
static boolean invokeIsTextureReference(String name) {
throw new AssertionError();
}
} }

View File

@@ -3,9 +3,8 @@ package org.geysermc.rainbow.pack;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
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.pack.animation.BedrockAnimation; import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext;
import org.geysermc.rainbow.pack.attachable.BedrockAttachable; import org.geysermc.rainbow.pack.attachable.BedrockAttachable;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
@@ -13,15 +12,14 @@ import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream; import java.util.stream.Stream;
public record BedrockItem(ResourceLocation identifier, String textureName, ResourceLocation texture, boolean exportTexture, Optional<BedrockAttachable> attachable, public record BedrockItem(ResourceLocation identifier, String textureName, BedrockGeometryContext geometry, Optional<BedrockAttachable> attachable) {
Optional<BedrockGeometry> geometry, Optional<BedrockAnimation> animation) {
public List<CompletableFuture<?>> save(PackSerializer serializer, Path attachableDirectory, Path geometryDirectory, Path animationDirectory) { public List<CompletableFuture<?>> save(PackSerializer serializer, Path attachableDirectory, Path geometryDirectory, Path animationDirectory) {
return Stream.concat( return Stream.concat(
attachable.stream().map(present -> present.save(serializer, attachableDirectory)), attachable.stream().map(present -> present.save(serializer, attachableDirectory)),
Stream.concat( Stream.concat(
geometry.stream().map(present -> present.save(serializer, geometryDirectory)), geometry.geometry().stream().map(present -> present.save(serializer, geometryDirectory)),
animation.stream().map(present -> present.save(serializer, animationDirectory, Rainbow.fileSafeResourceLocation(identifier))) geometry.animation().stream().map(context -> context.animation().save(serializer, animationDirectory, Rainbow.fileSafeResourceLocation(identifier)))
) )
).toList(); ).toList();
} }

View File

@@ -12,6 +12,8 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.CustomModelData; import net.minecraft.world.item.component.CustomModelData;
import org.geysermc.rainbow.CodecUtil; import org.geysermc.rainbow.CodecUtil;
import org.geysermc.rainbow.PackConstants; import org.geysermc.rainbow.PackConstants;
import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.image.NativeImageUtil;
import org.geysermc.rainbow.mapping.AssetResolver; import org.geysermc.rainbow.mapping.AssetResolver;
import org.geysermc.rainbow.mapping.BedrockItemMapper; import org.geysermc.rainbow.mapping.BedrockItemMapper;
import org.geysermc.rainbow.mapping.PackContext; import org.geysermc.rainbow.mapping.PackContext;
@@ -19,9 +21,11 @@ import org.geysermc.rainbow.mapping.PackSerializer;
import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; import org.geysermc.rainbow.mapping.geometry.GeometryRenderer;
import org.geysermc.rainbow.mapping.geometry.NoopGeometryRenderer; import org.geysermc.rainbow.mapping.geometry.NoopGeometryRenderer;
import org.geysermc.rainbow.definition.GeyserMappings; import org.geysermc.rainbow.definition.GeyserMappings;
import org.geysermc.rainbow.mapping.geometry.TextureHolder;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
@@ -41,7 +45,7 @@ public class BedrockPack {
private final BedrockTextures.Builder itemTextures = BedrockTextures.builder(); private final BedrockTextures.Builder itemTextures = BedrockTextures.builder();
private final Set<BedrockItem> bedrockItems = new HashSet<>(); private final Set<BedrockItem> bedrockItems = new HashSet<>();
private final Set<ResourceLocation> texturesToExport = new HashSet<>(); private final Set<TextureHolder> texturesToExport = new HashSet<>();
private final Set<ResourceLocation> modelsMapped = new HashSet<>(); private final Set<ResourceLocation> modelsMapped = new HashSet<>();
private final IntSet customModelDataMapped = new IntOpenHashSet(); private final IntSet customModelDataMapped = new IntOpenHashSet();
@@ -59,9 +63,7 @@ public class BedrockPack {
// Not reading existing item mappings/texture atlas for now since that doesn't work all that well yet // Not reading existing item mappings/texture atlas for now since that doesn't work all that well yet
this.context = new PackContext(new GeyserMappings(), paths, item -> { this.context = new PackContext(new GeyserMappings(), paths, item -> {
itemTextures.withItemTexture(item); itemTextures.withItemTexture(item);
if (item.exportTexture()) { texturesToExport.add(item.geometry().texture());
texturesToExport.add(item.texture());
}
bedrockItems.add(item); bedrockItems.add(item);
}, assetResolver, geometryRenderer, texturesToExport::add, reportSuccesses); }, assetResolver, geometryRenderer, texturesToExport::add, reportSuccesses);
this.reporter = reporter; this.reporter = reporter;
@@ -130,8 +132,27 @@ public class BedrockPack {
futures.addAll(item.save(serializer, paths.attachables(), paths.geometry(), paths.animation())); futures.addAll(item.save(serializer, paths.attachables(), paths.geometry(), paths.animation()));
} }
for (ResourceLocation texture : texturesToExport) { for (TextureHolder texture : texturesToExport) {
futures.add(serializer.saveTexture(texture, paths.packRoot().resolve(BedrockTextures.TEXTURES_FOLDER + texture.getPath() + ".png"))); ResourceLocation textureLocation = Rainbow.decorateTextureLocation(texture.location());
texture.supplier()
.flatMap(image -> {
try {
return Optional.of(NativeImageUtil.writeToByteArray(image.get()));
} catch (IOException exception) {
// TODO log
return Optional.empty();
}
})
.or(() -> {
try (InputStream textureStream = context.assetResolver().openAsset(textureLocation)) {
return Optional.of(textureStream.readAllBytes());
} catch (IOException exception) {
// TODO log
return Optional.empty();
}
})
.map(bytes -> serializer.saveTexture(bytes, paths.packRoot().resolve(textureLocation.getPath())))
.ifPresent(futures::add);
} }
if (paths.zipOutput().isPresent()) { if (paths.zipOutput().isPresent()) {

View File

@@ -33,7 +33,7 @@ public record BedrockTextures(Map<String, String> textures) {
private final Map<String, String> textures = new HashMap<>(); private final Map<String, String> textures = new HashMap<>();
public Builder withItemTexture(BedrockItem item) { public Builder withItemTexture(BedrockItem item) {
return withTexture(item.textureName(), TEXTURES_FOLDER + item.texture().getPath()); return withTexture(item.textureName(), TEXTURES_FOLDER + item.geometry().texture().location().getPath());
} }
public Builder withTexture(String name, String texture) { public Builder withTexture(String name, String texture) {

View File

@@ -5,10 +5,16 @@
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "JAVA_21",
"client": [ "client": [
"LateBoundIdMapperAccessor", "LateBoundIdMapperAccessor",
"NativeImageAccessor",
"RangeSelectItemModelAccessor", "RangeSelectItemModelAccessor",
"SpriteContentsAccessor",
"SpriteLoaderAccessor",
"TextureSlotsAccessor" "TextureSlotsAccessor"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1
} },
"mixins": [
"FaceBakeryAccessor"
]
} }