1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-12-04 23:40:00 +00:00

Split some textures into sprite sheets

- Split buttons.png into individual textures.
 - Split corners_xyz.png into the following:

   - borders_xyz.png: A nine-sliced texture of the computer borders.
   - pocket_bottom_xyz.png: A horizontally 3-sliced texture of the
     bottom part of a pocket computer.
   - sidebar_xyz.png: A vertically 3-sliced texture of the computer
     sidebar.

While not splitting the sliced textures into smaller ones may seem a
little odd, it's consistent with what vanilla does in 1.20.2, and I
think will make editing them easier than juggling 9 textures.

I do want to make this more data-driven in the future, but that will
have to wait until the changes in 1.20.2.

This also adds a tools/update-resources.py program, which performs this
transformation on a given resource pack.
This commit is contained in:
Jonathan Coates 2023-08-27 18:02:51 +01:00
parent 6dfdeb9321
commit 53546b9f57
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
37 changed files with 556 additions and 161 deletions

View File

@ -21,6 +21,7 @@ import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
import dan200.computercraft.shared.media.items.DiskItem;
import dan200.computercraft.shared.media.items.TreasureDiskItem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.item.ItemColor;
import net.minecraft.client.gui.screens.MenuScreens;
import net.minecraft.client.multiplayer.ClientLevel;
@ -30,6 +31,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ResourceProvider;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.Item;
@ -107,6 +109,10 @@ public final class ClientRegistry {
for (var item : items) ItemProperties.register(item.get(), id, getter);
}
public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) {
register.accept(GuiSprites.initialise(minecraft.getTextureManager()));
}
private static final String[] EXTRA_MODELS = new String[]{
"block/turtle_colour",
"block/turtle_elf_overlay",

View File

@ -5,15 +5,18 @@
package dan200.computercraft.client.gui;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.Tesselator;
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.SpriteRenderer;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER;
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
/**
* A GUI for computers which renders the terminal (and border), but with no UI elements.
@ -39,10 +42,16 @@ public final class ComputerScreen<T extends AbstractComputerMenu> extends Abstra
public void renderBg(PoseStack stack, float partialTicks, int mouseX, int mouseY) {
// Draw a border around the terminal
var terminal = getTerminal();
var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
var spriteRenderer = SpriteRenderer.createForGui(stack, buffers.getBuffer(RenderTypes.GUI_SPRITES));
var computerTextures = GuiSprites.getComputerTextures(family);
ComputerBorderRenderer.render(
stack.last().pose(), ComputerBorderRenderer.getTexture(family), terminal.getX(), terminal.getY(),
FULL_BRIGHT_LIGHTMAP, terminal.getWidth(), terminal.getHeight()
spriteRenderer, computerTextures,
terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false
);
ComputerSidebar.renderBackground(stack, leftPos, topPos + sidebarYOffset);
ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset);
buffers.endBatch();
}
}

View File

@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.gui;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.data.client.ClientDataProviders;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.client.resources.TextureAtlasHolder;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.stream.Stream;
/**
* Sprite sheet for all GUI texutres in the mod.
*/
public final class GuiSprites extends TextureAtlasHolder {
public static final ResourceLocation SPRITE_SHEET = new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui");
public static final ResourceLocation TEXTURE = SPRITE_SHEET.withPath(x -> "textures/atlas/" + x + ".png");
public static final ButtonTextures TURNED_OFF = button("turned_off");
public static final ButtonTextures TURNED_ON = button("turned_on");
public static final ButtonTextures TERMINATE = button("terminate");
public static final ComputerTextures COMPUTER_NORMAL = computer("normal", true, true);
public static final ComputerTextures COMPUTER_ADVANCED = computer("advanced", true, true);
public static final ComputerTextures COMPUTER_COMMAND = computer("command", false, true);
public static final ComputerTextures COMPUTER_COLOUR = computer("colour", true, false);
private static ButtonTextures button(String name) {
return new ButtonTextures(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover")
);
}
private static ComputerTextures computer(String name, boolean pocket, boolean sidebar) {
return new ComputerTextures(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/border_" + name),
pocket ? new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/pocket_bottom_" + name) : null,
sidebar ? new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sidebar_" + name) : null
);
}
private static @Nullable GuiSprites instance;
private GuiSprites(TextureManager textureManager) {
super(textureManager, TEXTURE, SPRITE_SHEET);
}
/**
* Initialise the singleton {@link GuiSprites} instance.
*
* @param textureManager The current texture manager.
* @return The singleton {@link GuiSprites} instance, to register as resource reload listener.
*/
public static GuiSprites initialise(TextureManager textureManager) {
if (instance != null) throw new IllegalStateException("GuiSprites has already been initialised");
return instance = new GuiSprites(textureManager);
}
/**
* Lookup a texture on the atlas.
*
* @param texture The texture to find.
* @return The sprite on the atlas.
*/
public static TextureAtlasSprite get(ResourceLocation texture) {
if (instance == null) throw new IllegalStateException("GuiSprites has not been initialised");
return instance.getSprite(texture);
}
/**
* Get the appropriate textures to use for a particular computer family.
*
* @param family The computer family.
* @return The family-specific textures.
*/
public static ComputerTextures getComputerTextures(ComputerFamily family) {
return switch (family) {
case NORMAL -> COMPUTER_NORMAL;
case ADVANCED -> COMPUTER_ADVANCED;
case COMMAND -> COMPUTER_COMMAND;
};
}
/**
* A set of sprites for a button, with both a normal and "active" state.
*
* @param normal The normal texture for the button.
* @param active The texture for the button when it is active (hovered or focused).
*/
public record ButtonTextures(ResourceLocation normal, ResourceLocation active) {
public TextureAtlasSprite get(boolean active) {
return GuiSprites.get(active ? this.active : normal);
}
public Stream<ResourceLocation> textures() {
return Stream.of(normal, active);
}
}
/**
* Set the set of sprites for a computer family.
*
* @param border The texture for the computer's border.
* @param pocketBottom The texture for the bottom of a pocket computer.
* @param sidebar The texture for the computer sidebar.
* @see ComputerBorderRenderer
* @see ClientDataProviders
*/
public record ComputerTextures(
ResourceLocation border,
@Nullable ResourceLocation pocketBottom,
@Nullable ResourceLocation sidebar
) {
public Stream<ResourceLocation> textures() {
return Stream.of(border, pocketBottom, sidebar).filter(Objects::nonNull);
}
}
}

View File

@ -6,13 +6,16 @@ package dan200.computercraft.client.gui;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.Tesselator;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.SpriteRenderer;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
@ -60,7 +63,10 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
);
}
RenderSystem.setShaderTexture(0, advanced ? ComputerBorderRenderer.BACKGROUND_ADVANCED : ComputerBorderRenderer.BACKGROUND_NORMAL);
ComputerSidebar.renderBackground(transform, leftPos, topPos + sidebarYOffset);
// Render sidebar
var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
var spriteRenderer = SpriteRenderer.createForGui(transform, buffers.getBuffer(RenderTypes.GUI_SPRITES));
ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset);
buffers.endBatch();
}
}

View File

@ -4,16 +4,13 @@
package dan200.computercraft.client.gui.widgets;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage;
import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.client.render.SpriteRenderer;
import dan200.computercraft.shared.computer.core.InputHandler;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
@ -22,22 +19,18 @@ import java.util.function.Consumer;
* Registers buttons to interact with a computer.
*/
public final class ComputerSidebar {
private static final ResourceLocation TEXTURE = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/buttons.png");
private static final int TEX_SIZE = 64;
private static final int ICON_WIDTH = 12;
private static final int ICON_HEIGHT = 12;
private static final int ICON_MARGIN = 2;
private static final int ICON_TEX_Y_DIFF = 14;
private static final int CORNERS_BORDER = 3;
private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN;
private static final int BUTTONS = 2;
private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2;
private static final int TEX_HEIGHT = 14;
private ComputerSidebar() {
}
@ -51,16 +44,18 @@ public final class ComputerSidebar {
Component.translatable("gui.computercraft.tooltip.turn_off.key")
);
add.accept(new DynamicImageButton(
x, y, ICON_WIDTH, ICON_HEIGHT, () -> isOn.getAsBoolean() ? 15 : 1, 1, ICON_TEX_Y_DIFF,
TEXTURE, TEX_SIZE, TEX_SIZE, b -> toggleComputer(isOn, input),
x, y, ICON_WIDTH, ICON_HEIGHT,
h -> isOn.getAsBoolean() ? GuiSprites.TURNED_ON.get(h) : GuiSprites.TURNED_OFF.get(h),
b -> toggleComputer(isOn, input),
() -> isOn.getAsBoolean() ? turnOff : turnOn
));
y += ICON_HEIGHT + ICON_MARGIN * 2;
add.accept(new DynamicImageButton(
x, y, ICON_WIDTH, ICON_HEIGHT, 29, 1, ICON_TEX_Y_DIFF,
TEXTURE, TEX_SIZE, TEX_SIZE, b -> input.queueEvent("terminate"),
x, y, ICON_WIDTH, ICON_HEIGHT,
GuiSprites.TERMINATE::get,
b -> input.queueEvent("terminate"),
new HintedMessage(
Component.translatable("gui.computercraft.tooltip.terminate"),
Component.translatable("gui.computercraft.tooltip.terminate.key")
@ -68,22 +63,12 @@ public final class ComputerSidebar {
));
}
public static void renderBackground(PoseStack transform, int x, int y) {
Screen.blit(transform,
x, y, 0, 102, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER,
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
);
public static void renderBackground(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y) {
var texture = textures.sidebar();
if (texture == null) throw new NullPointerException(textures + " has no sidebar texture");
var sprite = GuiSprites.get(texture);
Screen.blit(transform,
x, y + FULL_BORDER, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT - FULL_BORDER * 2,
0, 107, AbstractComputerMenu.SIDEBAR_WIDTH, 4,
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
);
Screen.blit(transform,
x, y + HEIGHT - FULL_BORDER, 0, 111, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER,
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
);
renderer.blitVerticalSliced(sprite, x, y, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT, FULL_BORDER, FULL_BORDER, TEX_HEIGHT);
}
private static void toggleComputer(BooleanSupplier isOn, InputHandler input) {

View File

@ -6,14 +6,14 @@ package dan200.computercraft.client.gui.widgets;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import it.unimi.dsi.fastutil.booleans.Boolean2ObjectFunction;
import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
/**
@ -21,50 +21,33 @@ import java.util.function.Supplier;
* dynamically.
*/
public class DynamicImageButton extends Button {
private final ResourceLocation texture;
private final IntSupplier xTexStart;
private final int yTexStart;
private final int yDiffTex;
private final int textureWidth;
private final int textureHeight;
private final Boolean2ObjectFunction<TextureAtlasSprite> texture;
private final Supplier<HintedMessage> message;
public DynamicImageButton(
int x, int y, int width, int height, int xTexStart, int yTexStart, int yDiffTex,
ResourceLocation texture, int textureWidth, int textureHeight,
OnPress onPress, HintedMessage message
int x, int y, int width, int height, Boolean2ObjectFunction<TextureAtlasSprite> texture, OnPress onPress,
HintedMessage message
) {
this(
x, y, width, height, () -> xTexStart, yTexStart, yDiffTex,
texture, textureWidth, textureHeight,
onPress, () -> message
);
this(x, y, width, height, texture, onPress, () -> message);
}
public DynamicImageButton(
int x, int y, int width, int height, IntSupplier xTexStart, int yTexStart, int yDiffTex,
ResourceLocation texture, int textureWidth, int textureHeight,
int x, int y, int width, int height,
Boolean2ObjectFunction<TextureAtlasSprite> texture,
OnPress onPress, Supplier<HintedMessage> message
) {
super(x, y, width, height, Component.empty(), onPress, DEFAULT_NARRATION);
this.textureWidth = textureWidth;
this.textureHeight = textureHeight;
this.xTexStart = xTexStart;
this.yTexStart = yTexStart;
this.yDiffTex = yDiffTex;
this.texture = texture;
this.message = message;
}
@Override
public void renderWidget(PoseStack stack, int mouseX, int mouseY, float partialTicks) {
RenderSystem.setShaderTexture(0, texture);
var texture = this.texture.get(isHoveredOrFocused());
RenderSystem.setShaderTexture(0, texture.atlasLocation());
RenderSystem.disableDepthTest();
var yTex = yTexStart;
if (isHoveredOrFocused()) yTex += yDiffTex;
blit(stack, getX(), getY(), xTexStart.getAsInt(), yTex, width, height, textureWidth, textureHeight);
blit(stack, getX(), getY(), 0, width, height, texture);
RenderSystem.enableDepthTest();
}

View File

@ -4,25 +4,17 @@
package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.Tesselator;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.resources.ResourceLocation;
import org.joml.Matrix4f;
import dan200.computercraft.client.gui.GuiSprites;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import static dan200.computercraft.client.render.SpriteRenderer.u;
import static dan200.computercraft.client.render.SpriteRenderer.v;
/**
* Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or
* {@linkplain PocketItemRenderer in-hand pocket computers}.
*/
public class ComputerBorderRenderer {
public static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_normal.png");
public static final ResourceLocation BACKGROUND_ADVANCED = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_advanced.png");
public static final ResourceLocation BACKGROUND_COMMAND = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_command.png");
public static final ResourceLocation BACKGROUND_COLOUR = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_colour.png");
public final class ComputerBorderRenderer {
/**
* The margin between the terminal and its border.
*/
@ -33,100 +25,51 @@ public class ComputerBorderRenderer {
*/
public static final int BORDER = 12;
private static final int CORNER_TOP_Y = 28;
private static final int CORNER_BOTTOM_Y = CORNER_TOP_Y + BORDER;
private static final int CORNER_LEFT_X = BORDER;
private static final int CORNER_RIGHT_X = CORNER_LEFT_X + BORDER;
private static final int BORDER_RIGHT_X = 36;
private static final int LIGHT_BORDER_Y = 56;
private static final int LIGHT_CORNER_Y = 80;
public static final int LIGHT_HEIGHT = 8;
public static final int TEX_SIZE = 256;
private static final float TEX_SCALE = 1 / (float) TEX_SIZE;
private static final int TEX_SIZE = 36;
private final Matrix4f transform;
private final VertexConsumer builder;
private final int light;
private final int z;
private final float r, g, b;
public ComputerBorderRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, float r, float g, float b) {
this.transform = transform;
this.builder = builder;
this.z = z;
this.light = light;
this.r = r;
this.g = g;
this.b = b;
private ComputerBorderRenderer() {
}
public static ResourceLocation getTexture(ComputerFamily family) {
return switch (family) {
case NORMAL -> BACKGROUND_NORMAL;
case ADVANCED -> BACKGROUND_ADVANCED;
case COMMAND -> BACKGROUND_COMMAND;
};
}
public static RenderType getRenderType(ResourceLocation location) {
// See note in RenderTypes about why we use text rather than anything intuitive.
return RenderType.text(location);
}
public static void render(Matrix4f transform, ResourceLocation location, int x, int y, int light, int width, int height) {
var source = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
render(transform, source.getBuffer(getRenderType(location)), x, y, 1, light, width, height, false, 1, 1, 1);
source.endBatch();
}
public static void render(Matrix4f transform, VertexConsumer buffer, int x, int y, int z, int light, int width, int height, boolean withLight, float r, float g, float b) {
new ComputerBorderRenderer(transform, buffer, z, light, r, g, b).doRender(x, y, width, height, withLight);
}
public void doRender(int x, int y, int width, int height, boolean withLight) {
public static void render(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y, int width, int height, boolean withLight) {
var endX = x + width;
var endY = y + height;
// Vertical bars
renderLine(x - BORDER, y, 0, CORNER_TOP_Y, BORDER, endY - y);
renderLine(endX, y, BORDER_RIGHT_X, CORNER_TOP_Y, BORDER, endY - y);
var border = GuiSprites.get(textures.border());
// Top bar
renderLine(x, y - BORDER, 0, 0, endX - x, BORDER);
renderCorner(x - BORDER, y - BORDER, CORNER_LEFT_X, CORNER_TOP_Y);
renderCorner(endX, y - BORDER, CORNER_RIGHT_X, CORNER_TOP_Y);
blitBorder(renderer, border, x - BORDER, y - BORDER, 0, 0, BORDER, BORDER);
blitBorder(renderer, border, x, y - BORDER, BORDER, 0, width, BORDER);
blitBorder(renderer, border, endX, y - BORDER, BORDER * 2, 0, BORDER, BORDER);
// Vertical bars
blitBorder(renderer, border, x - BORDER, y, 0, BORDER, BORDER, height);
blitBorder(renderer, border, endX, y, BORDER * 2, BORDER, BORDER, height);
// Bottom bar. We allow for drawing a stretched version, which allows for additional elements (such as the
// pocket computer's lights).
if (withLight) {
renderTexture(x, endY, 0, LIGHT_BORDER_Y, endX - x, BORDER + LIGHT_HEIGHT, BORDER, BORDER + LIGHT_HEIGHT);
renderTexture(x - BORDER, endY, CORNER_LEFT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT);
renderTexture(endX, endY, CORNER_RIGHT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT);
var pocketBottomTexture = textures.pocketBottom();
if (pocketBottomTexture == null) throw new NullPointerException(textures + " has no pocket texture");
var pocketBottom = GuiSprites.get(pocketBottomTexture);
renderer.blitHorizontalSliced(
pocketBottom, x - BORDER, endY, width + BORDER * 2, BORDER + LIGHT_HEIGHT,
BORDER, BORDER, BORDER * 3
);
} else {
renderLine(x, endY, 0, BORDER, endX - x, BORDER);
renderCorner(x - BORDER, endY, CORNER_LEFT_X, CORNER_BOTTOM_Y);
renderCorner(endX, endY, CORNER_RIGHT_X, CORNER_BOTTOM_Y);
blitBorder(renderer, border, x - BORDER, endY, 0, BORDER * 2, BORDER, BORDER);
blitBorder(renderer, border, x, endY, BORDER, BORDER * 2, width, BORDER);
blitBorder(renderer, border, endX, endY, BORDER * 2, BORDER * 2, BORDER, BORDER);
}
}
private void renderCorner(int x, int y, int u, int v) {
renderTexture(x, y, u, v, BORDER, BORDER, BORDER, BORDER);
}
private void renderLine(int x, int y, int u, int v, int width, int height) {
renderTexture(x, y, u, v, width, height, BORDER, BORDER);
}
private void renderTexture(int x, int y, int u, int v, int width, int height) {
renderTexture(x, y, u, v, width, height, width, height);
}
private void renderTexture(int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) {
builder.vertex(transform, x, y + height, z).color(r, g, b, 1.0f).uv(u * TEX_SCALE, (v + textureHeight) * TEX_SCALE).uv2(light).endVertex();
builder.vertex(transform, x + width, y + height, z).color(r, g, b, 1.0f).uv((u + textureWidth) * TEX_SCALE, (v + textureHeight) * TEX_SCALE).uv2(light).endVertex();
builder.vertex(transform, x + width, y, z).color(r, g, b, 1.0f).uv((u + textureWidth) * TEX_SCALE, v * TEX_SCALE).uv2(light).endVertex();
builder.vertex(transform, x, y, z).color(r, g, b, 1.0f).uv(u * TEX_SCALE, v * TEX_SCALE).uv2(light).endVertex();
private static void blitBorder(SpriteRenderer renderer, TextureAtlasSprite sprite, int x, int y, int u, int v, int width, int height) {
renderer.blit(
x, y, width, height,
u(sprite, u, TEX_SIZE), v(sprite, v, TEX_SIZE),
u(sprite, u + BORDER, TEX_SIZE), v(sprite, v + BORDER, TEX_SIZE)
);
}
}

View File

@ -6,6 +6,7 @@ package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.util.Colour;
@ -72,13 +73,14 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
}
private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) {
var texture = colour != -1 ? ComputerBorderRenderer.BACKGROUND_COLOUR : ComputerBorderRenderer.getTexture(family);
var texture = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family);
var r = ((colour >>> 16) & 0xFF) / 255.0f;
var g = ((colour >>> 8) & 0xFF) / 255.0f;
var b = (colour & 0xFF) / 255.0f;
var r = (colour >>> 16) & 0xFF;
var g = (colour >>> 8) & 0xFF;
var b = colour & 0xFF;
ComputerBorderRenderer.render(transform, render.getBuffer(ComputerBorderRenderer.getRenderType(texture)), 0, 0, 0, light, width, height, true, r, g, b);
var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(RenderTypes.GUI_SPRITES), 0, light, r, g, b);
ComputerBorderRenderer.render(spriteRenderer, texture, 0, 0, width, height, true);
}
private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) {

View File

@ -7,6 +7,7 @@ package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.VertexFormat;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import net.minecraft.client.renderer.GameRenderer;
@ -53,6 +54,11 @@ public class RenderTypes {
*/
public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(new ResourceLocation("computercraft", "textures/gui/printout.png"));
/**
* Render type for {@linkplain GuiSprites GUI sprites}.
*/
public static final RenderType GUI_SPRITES = RenderType.text(GuiSprites.TEXTURE);
public static MonitorTextureBufferShader getMonitorTextureBufferShader() {
if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered");
return monitorTboShader;

View File

@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.gui.GuiComponent;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import org.joml.Matrix4f;
/**
* A {@link GuiComponent}-equivalent which is suitable for both rendering in to a GUI and in-world (as part of an entity
* renderer).
* <p>
* This batches all render calls together, though requires that all {@link TextureAtlasSprite}s are on the same sprite
* sheet.
*/
public class SpriteRenderer {
private final Matrix4f transform;
private final VertexConsumer builder;
private final int light;
private final int z;
private final int r, g, b;
public SpriteRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, int r, int g, int b) {
this.transform = transform;
this.builder = builder;
this.z = z;
this.light = light;
this.r = r;
this.g = g;
this.b = b;
}
public static SpriteRenderer createForGui(PoseStack stack, VertexConsumer builder) {
return new SpriteRenderer(stack.last().pose(), builder, 0, RenderTypes.FULL_BRIGHT_LIGHTMAP, 255, 255, 255);
}
/**
* Render a single sprite.
*
* @param sprite The texture to draw.
* @param x The x position of the rectangle we'll draw.
* @param y The x position of the rectangle we'll draw.
* @param width The width of the rectangle we'll draw.
* @param height The height of the rectangle we'll draw.
*/
public void blit(TextureAtlasSprite sprite, int x, int y, int width, int height) {
blit(x, y, width, height, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1());
}
/**
* Render a horizontal 3-sliced texture (i.e. split into left, middle and right). Unlike {@link GuiComponent#blitNineSliced},
* the middle texture is stretched rather than repeated.
*
* @param sprite The texture to draw.
* @param x The x position of the rectangle we'll draw.
* @param y The x position of the rectangle we'll draw.
* @param width The width of the rectangle we'll draw.
* @param height The height of the rectangle we'll draw.
* @param leftBorder The width of the left border.
* @param rightBorder The width of the right border.
* @param textureWidth The width of the whole texture.
*/
public void blitHorizontalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int leftBorder, int rightBorder, int textureWidth) {
// TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does.
if (width < leftBorder + rightBorder) throw new IllegalArgumentException("width is less than two borders");
var centerStart = SpriteRenderer.u(sprite, leftBorder, textureWidth);
var centerEnd = SpriteRenderer.u(sprite, textureWidth - rightBorder, textureWidth);
blit(x, y, leftBorder, height, sprite.getU0(), sprite.getV0(), centerStart, sprite.getV1());
blit(x + leftBorder, y, width - leftBorder - rightBorder, height, centerStart, sprite.getV0(), centerEnd, sprite.getV1());
blit(x + width - rightBorder, y, rightBorder, height, centerEnd, sprite.getV0(), sprite.getU1(), sprite.getV1());
}
/**
* Render a vertical 3-sliced texture (i.e. split into top, middle and bottom). Unlike {@link GuiComponent#blitNineSliced},
* the middle texture is stretched rather than repeated.
*
* @param sprite The texture to draw.
* @param x The x position of the rectangle we'll draw.
* @param y The x position of the rectangle we'll draw.
* @param width The width of the rectangle we'll draw.
* @param height The height of the rectangle we'll draw.
* @param topBorder The height of the top border.
* @param bottomBorder The height of the bottom border.
* @param textureHeight The height of the whole texture.
*/
public void blitVerticalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int topBorder, int bottomBorder, int textureHeight) {
// TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does.
if (width < topBorder + bottomBorder) throw new IllegalArgumentException("height is less than two borders");
var centerStart = SpriteRenderer.v(sprite, topBorder, textureHeight);
var centerEnd = SpriteRenderer.v(sprite, textureHeight - bottomBorder, textureHeight);
blit(x, y, width, topBorder, sprite.getU0(), sprite.getV0(), sprite.getU1(), centerStart);
blit(x, y + topBorder, width, height - topBorder - bottomBorder, sprite.getU0(), centerStart, sprite.getU1(), centerEnd);
blit(x, y + height - bottomBorder, width, bottomBorder, sprite.getU0(), centerEnd, sprite.getU1(), sprite.getV1());
}
/**
* The low-level blit function, used to render a portion of the sprite sheet. Unlike other functions, this takes uvs rather than a single sprite.
*
* @param x The x position of the rectangle we'll draw.
* @param y The x position of the rectangle we'll draw.
* @param width The width of the rectangle we'll draw.
* @param height The height of the rectangle we'll draw.
* @param u0 The first U coordinate.
* @param v0 The first V coordinate.
* @param u1 The second U coordinate.
* @param v1 The second V coordinate.
*/
public void blit(
int x, int y, int width, int height, float u0, float v0, float u1, float v1) {
builder.vertex(transform, x, y + height, z).color(r, g, b, 255).uv(u0, v1).uv2(light).endVertex();
builder.vertex(transform, x + width, y + height, z).color(r, g, b, 255).uv(u1, v1).uv2(light).endVertex();
builder.vertex(transform, x + width, y, z).color(r, g, b, 255).uv(u1, v0).uv2(light).endVertex();
builder.vertex(transform, x, y, z).color(r, g, b, 255).uv(u0, v0).uv2(light).endVertex();
}
public static float u(TextureAtlasSprite sprite, int x, int width) {
return sprite.getU((double) x / width * 16);
}
public static float v(TextureAtlasSprite sprite, int y, int height) {
return sprite.getV((double) y / height * 16);
}
}

View File

@ -4,8 +4,10 @@
package dan200.computercraft.data.client;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.data.DataProviders;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
import net.minecraft.resources.ResourceLocation;
@ -13,6 +15,7 @@ import net.minecraft.server.packs.PackType;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
/**
* A version of {@link DataProviders} which relies on client-side classes.
@ -29,6 +32,17 @@ public final class ClientDataProviders {
new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()),
new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty())
));
out.accept(GuiSprites.SPRITE_SHEET, Stream.of(
// Buttons
GuiSprites.TURNED_OFF.textures(),
GuiSprites.TURNED_ON.textures(),
GuiSprites.TERMINATE.textures(),
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),
GuiSprites.COMPUTER_COMMAND.textures(),
GuiSprites.COMPUTER_COLOUR.textures()
).flatMap(x -> x).<SpriteSource>map(x -> new SingleFile(x, Optional.empty())).toList());
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

View File

@ -5,16 +5,25 @@
package dan200.computercraft.mixin.client;
import dan200.computercraft.client.ClientHooks;
import dan200.computercraft.client.ClientRegistry;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.main.GameConfig;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.server.packs.resources.ReloadableResourceManager;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Minecraft.class)
class MinecraftMixin {
@Shadow
@Final
private ReloadableResourceManager resourceManager;
@Inject(method = "clearLevel(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At("HEAD"))
@SuppressWarnings("UnusedMethod")
private void clearLevel(Screen screen, CallbackInfo ci) {
@ -26,4 +35,16 @@ class MinecraftMixin {
private void setLevel(ClientLevel screen, CallbackInfo ci) {
ClientHooks.onWorldUnload();
}
@Inject(
method = "<init>(Lnet/minecraft/client/main/GameConfig;)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/ResourceLoadStateTracker;startReload(Lnet/minecraft/client/ResourceLoadStateTracker$ReloadReason;Ljava/util/List;)V",
ordinal = 0
)
)
public void beforeInitialResourceReload(GameConfig gameConfig, CallbackInfo ci) {
ClientRegistry.registerReloadListeners(resourceManager::registerReloadListener, (Minecraft) (Object) this);
}
}

View File

@ -0,0 +1,20 @@
{
"sources": [
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_advanced"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_advanced"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_advanced"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_command"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_command"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_colour"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_colour"}
]
}

View File

@ -6,8 +6,10 @@ package dan200.computercraft.client;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.model.turtle.TurtleModelLoader;
import net.minecraft.client.Minecraft;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.ModelEvent;
import net.minecraftforge.client.event.RegisterClientReloadListenersEvent;
import net.minecraftforge.client.event.RegisterColorHandlersEvent;
import net.minecraftforge.client.event.RegisterShadersEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
@ -44,6 +46,11 @@ public final class ForgeClientRegistry {
ClientRegistry.registerItemColours(event::register);
}
@SubscribeEvent
public static void registerReloadListeners(RegisterClientReloadListenersEvent event) {
ClientRegistry.registerReloadListeners(event::registerReloadListener, Minecraft.getInstance());
}
@SubscribeEvent
public static void setupClient(FMLClientSetupEvent event) {
ClientRegistry.register();

View File

@ -0,0 +1,20 @@
{
"sources": [
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_advanced"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_advanced"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_advanced"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_command"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_command"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_colour"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_colour"}
]
}

115
tools/update-resources.py Normal file
View File

@ -0,0 +1,115 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
#
# SPDX-License-Identifier: MPL-2.0
"""
Upgrades textures to newer resource pack formats.
Currently implemented transformations:
- Split gui/corners_*.png and gui/buttons.png textures into smaller textures.
"""
from PIL import Image
import os
import pathlib
import argparse
def box(x: int, y: int, w: int, h: int):
return (x, y, x + w, y + h)
def unstitch_buttons(input_file: pathlib.Path):
"""Unstitch the button texture,"""
buttons = Image.open(input_file)
output_dir = input_file.parent / "buttons"
output_dir.mkdir(exist_ok=True)
buttons.crop(box(1, 1, 12, 12)).save(output_dir / "turned_off.png")
buttons.crop(box(1, 15, 12, 12)).save(output_dir / "turned_off_hover.png")
buttons.crop(box(15, 1, 12, 12)).save(output_dir / "turned_on.png")
buttons.crop(box(15, 15, 12, 12)).save(output_dir / "turned_on_hover.png")
buttons.crop(box(29, 1, 12, 12)).save(output_dir / "terminate.png")
buttons.crop(box(29, 15, 12, 12)).save(output_dir / "terminate_hover.png")
def unstitch_corners(input_file: pathlib.Path, family: str):
"""Unstitch the corners texture."""
input = Image.open(input_file)
output_dir = input_file.parent
border = Image.new("RGBA", (36, 36))
# Corners
border.paste(input.crop(box(12, 28, 12, 12)), box(00, 00, 12, 12))
border.paste(input.crop(box(24, 28, 12, 12)), box(24, 00, 12, 12))
border.paste(input.crop(box(12, 40, 12, 12)), box(00, 24, 12, 12))
border.paste(input.crop(box(24, 40, 12, 12)), box(24, 24, 12, 12))
# Horizontal bars
border.paste(input.crop(box(00, 00, 12, 12)), box(12, 00, 12, 12))
border.paste(input.crop(box(00, 12, 12, 12)), box(12, 24, 12, 12))
# Vertical bars
border.paste(input.crop(box(00, 28, 12, 12)), box(00, 12, 12, 12))
border.paste(input.crop(box(36, 28, 12, 12)), box(24, 12, 12, 12))
border.save(output_dir / f"border_{family}.png")
if family != "command":
# Fatter bottom pocket computer border.
pocket_computer = Image.new("RGBA", (36, 20))
# Middle
pocket_computer.paste(input.crop(box(00, 56, 12, 20)), box(12, 0, 12, 20))
# Corners
pocket_computer.paste(input.crop(box(12, 80, 12, 20)), box(00, 0, 12, 20))
pocket_computer.paste(input.crop(box(24, 80, 12, 20)), box(24, 0, 12, 20))
pocket_computer.save(output_dir / f"pocket_bottom_{family}.png")
if family != "colour":
# Sidebar
sidebar = Image.new("RGBA", (17, 14))
sidebar.paste(input.crop(box(0, 102, 17, 14)), box(0, 0, 17, 14))
sidebar.save(output_dir / f"sidebar_{family}.png")
def main() -> None:
spec = argparse.ArgumentParser()
spec.add_argument("dir", type=pathlib.Path)
dir: pathlib.Path = spec.parse_args().dir
texture_path = dir / "assets" / "computercraft" / "textures"
transformed: list[pathlib.Path] = []
buttons_path = texture_path / "gui" / "buttons.png"
if buttons_path.exists():
unstitch_buttons(buttons_path)
transformed.append(buttons_path)
for family in ("normal", "advanced", "command", "colour"):
path = texture_path / "gui" / f"corners_{family}.png"
if path.exists():
unstitch_corners(path, family)
transformed.append(path)
if len(transformed) == 0:
print("No files were transformed")
return
print("The following files may be deleted")
for file in transformed:
print(f" - {file}")
if input("Do so now? [y/N]").lower() == "y":
for file in transformed:
os.remove(file)
if __name__ == "__main__":
main()