diff --git a/fabric/src/main/resources/canvas.accesswidener b/fabric/src/main/resources/canvas.accesswidener
index 57c80d821..8fa06cffe 100644
--- a/fabric/src/main/resources/canvas.accesswidener
+++ b/fabric/src/main/resources/canvas.accesswidener
@@ -154,3 +154,5 @@ accessible field net/minecraft/client/renderer/entity/EntityRenderDispatcher SHA
accessible field net/minecraft/client/renderer/ShaderInstance name Ljava/lang/String;
extendable method com/mojang/blaze3d/pipeline/MainTarget createFrameBuffer (II)V
+
+accessible class net/minecraft/client/resources/model/ModelManager$ReloadState
diff --git a/fabric/src/main/resources/mixins.canvas.client.json b/fabric/src/main/resources/mixins.canvas.client.json
index 313c9038a..91b88b78f 100644
--- a/fabric/src/main/resources/mixins.canvas.client.json
+++ b/fabric/src/main/resources/mixins.canvas.client.json
@@ -33,6 +33,7 @@
"MixinLightTexture",
"MixinMinecraft",
"MixinModelBlockRenderer",
+ "MixinModelManager",
"MixinNativeImage",
"MixinParticle",
"MixinParticleEngine",
@@ -43,6 +44,7 @@
"MixinSimpleTexture",
"MixinSpriteContents",
"MixinSpriteInterpolation",
+ "MixinStitchResult",
"MixinTextureAtlas",
"MixinTextureSheetParticle",
"MixinTropicalFishModelA",
diff --git a/src/main/java/grondag/canvas/mixin/MixinModelManager.java b/src/main/java/grondag/canvas/mixin/MixinModelManager.java
new file mode 100644
index 000000000..abcbda7aa
--- /dev/null
+++ b/src/main/java/grondag/canvas/mixin/MixinModelManager.java
@@ -0,0 +1,60 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.mixin;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+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;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.resources.model.ModelManager;
+import net.minecraft.server.packs.resources.PreparableReloadListener;
+import net.minecraft.server.packs.resources.ResourceManager;
+import net.minecraft.util.profiling.ProfilerFiller;
+
+import grondag.canvas.texture.pbr.PbrLoader;
+
+@Mixin(ModelManager.class)
+public abstract class MixinModelManager {
+ @Shadow public abstract CompletableFuture reload(PreparableReloadListener.PreparationBarrier preparationBarrier, ResourceManager resourceManager, ProfilerFiller profilerFiller, ProfilerFiller profilerFiller2, Executor executor, Executor executor2);
+
+ private static ResourceManager canvas_resourceManager;
+
+ @Inject(method = "reload", at = @At("HEAD"))
+ private void onReload(PreparableReloadListener.PreparationBarrier preparationBarrier, ResourceManager resourceManager, ProfilerFiller profilerFiller, ProfilerFiller profilerFiller2, Executor executor, Executor executor2, CallbackInfoReturnable> cir) {
+ canvas_resourceManager = resourceManager;
+ }
+ @Inject(method = "apply", at = @At("HEAD"))
+ private void onApply(ModelManager.ReloadState reloadState, ProfilerFiller profilerFiller, CallbackInfo ci) {
+ if (canvas_resourceManager == null) {
+ PbrLoader.errorOrdering();
+ } else {
+ PbrLoader.reload(canvas_resourceManager, reloadState.atlasPreparations().values());
+ canvas_resourceManager = null;
+ }
+ }
+}
diff --git a/src/main/java/grondag/canvas/mixin/MixinStitchResult.java b/src/main/java/grondag/canvas/mixin/MixinStitchResult.java
new file mode 100644
index 000000000..558c86cff
--- /dev/null
+++ b/src/main/java/grondag/canvas/mixin/MixinStitchResult.java
@@ -0,0 +1,47 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.mixin;
+
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+
+import net.minecraft.client.renderer.texture.SpriteLoader;
+import net.minecraft.client.renderer.texture.TextureAtlas;
+import net.minecraft.client.resources.model.AtlasSet;
+
+import grondag.canvas.mixinterface.StitchResultExt;
+
+@Mixin(AtlasSet.StitchResult.class)
+public class MixinStitchResult implements StitchResultExt {
+ @Final @Shadow private TextureAtlas atlas;
+ @Final @Shadow private SpriteLoader.Preparations preparations;
+
+ @Override
+ public TextureAtlas canvas_atlas() {
+ return atlas;
+ }
+
+ @Override
+ public SpriteLoader.Preparations canvas_preparations() {
+ return preparations;
+ }
+}
diff --git a/src/main/java/grondag/canvas/mixinterface/StitchResultExt.java b/src/main/java/grondag/canvas/mixinterface/StitchResultExt.java
new file mode 100644
index 000000000..f1d5791ea
--- /dev/null
+++ b/src/main/java/grondag/canvas/mixinterface/StitchResultExt.java
@@ -0,0 +1,29 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.mixinterface;
+
+import net.minecraft.client.renderer.texture.SpriteLoader;
+import net.minecraft.client.renderer.texture.TextureAtlas;
+
+public interface StitchResultExt {
+ TextureAtlas canvas_atlas();
+ SpriteLoader.Preparations canvas_preparations();
+}
diff --git a/src/main/java/grondag/canvas/texture/pbr/ColorEncode.java b/src/main/java/grondag/canvas/texture/pbr/ColorEncode.java
new file mode 100644
index 000000000..855ddc4b3
--- /dev/null
+++ b/src/main/java/grondag/canvas/texture/pbr/ColorEncode.java
@@ -0,0 +1,49 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.texture.pbr;
+
+// TODO: this class has to already exist somewhere..
+public class ColorEncode {
+ static int encode(int r, int g, int b, int a) {
+ return ((r & 0xFF) << 0) | ((g & 0xFF) << 8) | ((b & 0xFF) << 16) | ((a & 0xFF) << 24);
+ }
+
+ static int r(int color) {
+ return (color >> 0) & 0xFF;
+ }
+
+ static int g(int color) {
+ return (color >> 8) & 0xFF;
+ }
+
+ static int b(int color) {
+ return (color >> 16) & 0xFF;
+ }
+
+ static int a(int color) {
+ return (color >> 24) & 0xFF;
+ }
+
+ static final int RED_BYTE_OFFSET = 0;
+ static final int GREEN_BYTE_OFFSET = 1;
+ static final int BLUE_BYTE_OFFSET = 2;
+ static final int ALPHA_BYTE_OFFSET = 3;
+}
diff --git a/src/main/java/grondag/canvas/texture/pbr/InputTextureManager.java b/src/main/java/grondag/canvas/texture/pbr/InputTextureManager.java
new file mode 100644
index 000000000..872666d2f
--- /dev/null
+++ b/src/main/java/grondag/canvas/texture/pbr/InputTextureManager.java
@@ -0,0 +1,158 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.texture.pbr;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+
+import com.mojang.blaze3d.platform.NativeImage;
+import com.mojang.blaze3d.platform.TextureUtil;
+
+import net.minecraft.client.renderer.texture.atlas.SpriteSource;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.ResourceManager;
+
+import grondag.canvas.CanvasMod;
+import grondag.canvas.render.CanvasTextureState;
+import grondag.canvas.varia.GFX;
+
+public class InputTextureManager {
+ final Object2ObjectOpenHashMap textures = new Object2ObjectOpenHashMap<>();
+ private final InputTexture EMPTY_INPUT = new InputTexture();
+
+ void addInputResource(ResourceManager resourceManager, ResourceLocation location) {
+ if (!textures.containsKey(location)) {
+ final var resource = resourceManager.getResource(location);
+ InputTexture added = EMPTY_INPUT;
+
+ if (resource.isPresent()) {
+ try (InputStream inputStream = resource.get().open()) {
+ added = new ImageInput(location, NativeImage.read(inputStream), true);
+ } catch (Throwable e) {
+ CanvasMod.LOG.error("Unable to load PBR texture " + location + " due to exception:\n" + e);
+ }
+ }
+
+ textures.put(location, added);
+ }
+ }
+
+ void addInputImage(ResourceLocation id, NativeImage image) {
+ var location = SpriteSource.TEXTURE_ID_CONVERTER.idToFile(id);
+
+ if (!textures.containsKey(location)) {
+ textures.put(location, new ImageInput(location, image, false));
+ }
+ }
+
+ InputTexture getInput(ResourceLocation location) {
+ return textures.get(location);
+ }
+
+ InputTexture getSpriteDefault(ResourceLocation id) {
+ var location = SpriteSource.TEXTURE_ID_CONVERTER.idToFile(id);
+
+ return textures.get(location);
+ }
+
+ void uploadInputs() {
+ GFX.pixelStore(GFX.GL_UNPACK_SKIP_PIXELS, 0);
+ GFX.pixelStore(GFX.GL_UNPACK_SKIP_ROWS, 0);
+ GFX.pixelStore(GFX.GL_UNPACK_ROW_LENGTH, 0);
+ GFX.pixelStore(GFX.GL_UNPACK_ALIGNMENT, 1);
+
+ for (var i : textures.values()) {
+ i.upload();
+ }
+ }
+
+ void clear() {
+ for (var entry : textures.values()) {
+ entry.close();
+ }
+
+ textures.clear();
+ }
+
+ static class InputTexture {
+ boolean present() {
+ return false;
+ }
+
+ int getTexId() {
+ return -1;
+ }
+
+ protected void upload() {
+ }
+
+ protected void close() {
+ }
+ }
+
+ static class ImageInput extends InputTexture {
+ final ResourceLocation location;
+ final NativeImage image;
+ final boolean autoClose;
+ private int texId = -1;
+
+ public ImageInput(ResourceLocation location, NativeImage image, boolean autoClose) {
+ this.location = location;
+ this.image = image;
+ this.autoClose = autoClose;
+ }
+
+ @Override
+ public boolean present() {
+ return texId != -1;
+ }
+
+ @Override
+ public int getTexId() {
+ return texId;
+ }
+
+ @Override
+ protected void upload() {
+ if (texId == -1) {
+ texId = TextureUtil.generateTextureId();
+ }
+
+ CanvasTextureState.bindTexture(texId);
+ GFX.texImage2D(GFX.GL_TEXTURE_2D, 0, GFX.GL_RGBA8, image.getWidth(), image.getHeight(), 0, image.format().glFormat(), GFX.GL_UNSIGNED_BYTE, (ByteBuffer) null);
+ image.upload(0, 0, 0, autoClose);
+ }
+
+ @Override
+ protected void close() {
+ if (texId != -1) {
+ TextureUtil.releaseTextureId(texId);
+ texId = -1;
+ }
+
+ if (autoClose) {
+ image.close();
+ }
+ }
+ }
+}
diff --git a/src/main/java/grondag/canvas/texture/pbr/PbrLoader.java b/src/main/java/grondag/canvas/texture/pbr/PbrLoader.java
new file mode 100644
index 000000000..990f08a6b
--- /dev/null
+++ b/src/main/java/grondag/canvas/texture/pbr/PbrLoader.java
@@ -0,0 +1,88 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.texture.pbr;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import net.minecraft.client.resources.model.AtlasSet;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.ResourceManager;
+
+import grondag.canvas.CanvasMod;
+import grondag.canvas.mixinterface.SpriteContentsExt;
+import grondag.canvas.mixinterface.StitchResultExt;
+import grondag.canvas.varia.GFX;
+
+public class PbrLoader {
+ static final InputTextureManager inputTextureManager = new InputTextureManager();
+
+ public static void errorOrdering() {
+ CanvasMod.LOG.error("Can't load PBR atlases due to load ordering error!");
+ }
+
+ public static void reload(ResourceManager manager, Collection atlasPreparations) {
+ var atlases = new ArrayList();
+ var debugProcess = new PbrProcess(new ResourceLocation("frex:shaders/func/normal_from_luminance.frag"));
+
+ for (var rawEntry : atlasPreparations) {
+ final var entry = (StitchResultExt) rawEntry;
+ final var atlas = entry.canvas_atlas();
+ final var preps = entry.canvas_preparations();
+
+ PbrMapAtlas pbrAtlas = new PbrMapAtlas(atlas.location(), preps.width(), preps.height(), preps.mipLevel());
+ atlases.add(pbrAtlas);
+
+ for (var spriteEntry : preps.regions().entrySet()) {
+ var sprite = spriteEntry.getValue();
+ var contents = spriteEntry.getValue().contents();
+ var contentsExt = (SpriteContentsExt) contents;
+ inputTextureManager.addInputImage(spriteEntry.getKey(), contentsExt.canvas_images()[0]);
+
+ var pbrSprite = new PbrMapSprite(atlas.location(), spriteEntry.getKey(), sprite.getX(), sprite.getY(), contents.width(), contents.height());
+ pbrAtlas.sprites.add(pbrSprite);
+
+ // debug
+ pbrSprite.withProcess(PbrMapSpriteLayer.NORMAL, debugProcess, new InputTextureManager.InputTexture[]{inputTextureManager.getSpriteDefault(spriteEntry.getKey())});
+
+ // CanvasMod.LOG.info(atlas.location() + ", " + spriteEntry.getKey() + ", " + spriteEntry.getValue().contentsExt().name());
+ }
+ }
+
+ // we are in Render thread the whole time
+ inputTextureManager.uploadInputs();
+
+ GFX.depthMask(false);
+ GFX.disableBlend();
+ GFX.disableCull();
+ GFX.disableDepthTest();
+
+ var context = new PbrProcess.DrawContext();
+ context.drawBuffer.bind();
+
+ for (var atlas : atlases) {
+ atlas.processAndUpload();
+ }
+
+ inputTextureManager.clear();
+ context.close();
+ }
+}
diff --git a/src/main/java/grondag/canvas/texture/pbr/PbrMapAtlas.java b/src/main/java/grondag/canvas/texture/pbr/PbrMapAtlas.java
new file mode 100644
index 000000000..9678a45cb
--- /dev/null
+++ b/src/main/java/grondag/canvas/texture/pbr/PbrMapAtlas.java
@@ -0,0 +1,204 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.texture.pbr;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Locale;
+
+import org.lwjgl.system.MemoryUtil;
+
+import com.mojang.blaze3d.platform.NativeImage;
+import com.mojang.blaze3d.platform.TextureUtil;
+import com.mojang.blaze3d.systems.RenderSystem;
+
+import net.minecraft.Util;
+import net.minecraft.client.Minecraft;
+import net.minecraft.resources.ResourceLocation;
+
+import grondag.canvas.CanvasMod;
+import grondag.canvas.config.Configurator;
+import grondag.canvas.render.CanvasTextureState;
+import grondag.canvas.varia.GFX;
+
+public class PbrMapAtlas {
+
+ static final int GL_TARGET = GFX.GL_TEXTURE_2D_ARRAY;
+ static final int GL_INTERNAL_FORMAT = GFX.GL_RGBA8;
+ static final int GL_FORMAT = GFX.GL_RGBA;
+ static final int GL_TYPE = GFX.GL_UNSIGNED_BYTE;
+
+ final ResourceLocation location;
+ final ArrayList sprites = new ArrayList<>();
+ final int width;
+ final int height;
+ final int lod;
+ int texId;
+
+ public PbrMapAtlas(ResourceLocation atlasLocation, int width, int height, int lod) {
+ this.location = atlasLocation;
+ this.width = width;
+ this.height = height;
+ this.lod = lod;
+ }
+
+ void processAndUpload() {
+ for (var sprite : sprites) {
+ sprite.process();
+ }
+
+ GFX.pixelStore(GFX.GL_PACK_SKIP_PIXELS, 0);
+ GFX.pixelStore(GFX.GL_PACK_SKIP_ROWS, 0);
+ GFX.pixelStore(GFX.GL_PACK_ROW_LENGTH, 0);
+
+ for (var sprite : sprites) {
+ sprite.download();
+ }
+
+ texId = TextureUtil.generateTextureId();
+ CanvasTextureState.bindTexture(GL_TARGET, texId);
+ GFX.objectLabel(GFX.GL_TEXTURE, texId, "IMG " + labelAtlas());
+
+ GFX.texParameter(GL_TARGET, GFX.GL_TEXTURE_MIN_FILTER, GFX.GL_NEAREST);
+ GFX.texParameter(GL_TARGET, GFX.GL_TEXTURE_MAG_FILTER, GFX.GL_NEAREST);
+ GFX.texParameter(GL_TARGET, GFX.GL_TEXTURE_WRAP_S, GFX.GL_CLAMP_TO_EDGE);
+ GFX.texParameter(GL_TARGET, GFX.GL_TEXTURE_WRAP_T, GFX.GL_CLAMP_TO_EDGE);
+
+ // if (lod > 0) {
+ // GFX.texParameter(GL_TARGET, GFX.GL_TEXTURE_MIN_LOD, 0);
+ // GFX.texParameter(GL_TARGET, GFX.GL_TEXTURE_MAX_LOD, lod);
+ // GFX.texParameter(GL_TARGET, GFX.GL_TEXTURE_MAX_LEVEL, lod);
+ // GFX.texParameter(GL_TARGET, GFX.GL_TEXTURE_LOD_BIAS, 0.0F);
+ // }
+
+ GFX.pixelStore(GFX.GL_UNPACK_SKIP_PIXELS, 0);
+ GFX.pixelStore(GFX.GL_UNPACK_SKIP_ROWS, 0);
+ GFX.pixelStore(GFX.GL_UNPACK_ROW_LENGTH, 0);
+ GFX.pixelStore(GFX.GL_UNPACK_ALIGNMENT, 4);
+
+ final int layerCount = PbrMapAtlasLayer.values().length;
+
+ // prepare. necessary?
+ GFX.texImage3D(GL_TARGET, 0, GL_INTERNAL_FORMAT, width, height, layerCount, 0, GL_FORMAT, GL_TYPE, null);
+
+ // TODO: lod, CPU side or GPU side?
+ final int w = width;
+ final int h = height;
+
+ for (var atlasLayer : PbrMapAtlasLayer.values()) {
+ ByteBuffer pixels = MemoryUtil.memAlloc(width * height * 4 * layerCount);
+
+ for (var sprite : sprites) {
+ for (int y = 0; y < sprite.height; y++) {
+ pixels.position(sprite.x * 4 + ((sprite.y + y) * width * 4));
+
+ for (int x = 0; x < sprite.width; x++) {
+ // pixels.putInt(ColorEncode.encode(sprite.r(PbrMapSpriteLayer.NORMAL, x, y), sprite.g(PbrMapSpriteLayer.NORMAL, x, y), sprite.b(PbrMapSpriteLayer.NORMAL, x, y), 255));
+ pixels.putInt(ColorEncode.encode(atlasLayer.swizzler.r(sprite, x, y), atlasLayer.swizzler.g(sprite, x, y), atlasLayer.swizzler.b(sprite, x, y), atlasLayer.swizzler.a(sprite, x, y)));
+ // pixels.putInt(ColorEncode.encode(atlasLayer.swizzler.r(sprite, x, y), atlasLayer.swizzler.g(sprite, x, y), atlasLayer.swizzler.b(sprite, x, y), 255));
+ }
+ }
+ }
+
+ pixels.position(0);
+ GFX.glTexSubImage3D(PbrMapAtlas.GL_TARGET, 0, 0, 0, atlasLayer.layer, w, h, 1, PbrMapAtlas.GL_FORMAT, PbrMapAtlas.GL_TYPE, pixels);
+
+ MemoryUtil.memFree(pixels);
+ }
+
+ for (var sprite : sprites) {
+ sprite.close();
+ }
+
+ // debug output
+ if (Configurator.debugSpriteAtlas) {
+ debugOutput();
+ }
+ }
+
+ public void debugOutput() {
+ RenderSystem.assertOnRenderThreadOrInit();
+
+ final int layerCount = PbrMapAtlasLayer.values().length;
+ final ByteBuffer debugPixels = MemoryUtil.memAlloc(width * height * 4 * layerCount);
+ CanvasTextureState.bindTexture(GL_TARGET, texId);
+ GFX.pixelStore(GFX.GL_PACK_SKIP_PIXELS, 0);
+ GFX.pixelStore(GFX.GL_PACK_SKIP_ROWS, 0);
+ GFX.pixelStore(GFX.GL_PACK_ROW_LENGTH, 0);
+ GFX.pixelStore(GFX.GL_PACK_ALIGNMENT, 4);
+ GFX.glGetTexImage(GL_TARGET, 0, GL_FORMAT, GL_TYPE, debugPixels);
+
+ Util.ioPool().execute(() -> {
+ NativeImage[] images = new NativeImage[layerCount];
+
+ try {
+ int i = 0;
+
+ for (var layer : PbrMapAtlasLayer.values()) {
+ var location = label(layer);
+ final Path path = Minecraft.getInstance().gameDirectory.toPath().normalize().resolve("atlas_debug");
+ final File atlasDir = path.toFile();
+
+ if (!atlasDir.exists()) {
+ atlasDir.mkdir();
+ CanvasMod.LOG.info("Created atlas debug output folder" + atlasDir.toString());
+ }
+
+ final File file = new File(atlasDir.getAbsolutePath() + File.separator + location.replaceAll("[_/:]", "_"));
+ images[i] = new NativeImage(width, height, false);
+
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ int pixel = debugPixels.getInt((i * width * height * 4) + x * 4 + width * y * 4);
+ // we have faith in NativeImage byte ordering as the benchmark
+ images[i].setPixelRGBA(x, y, pixel);
+ }
+ }
+
+ images[i].writeToFile(file);
+
+ i++;
+ }
+
+ } catch (final Exception e) {
+ CanvasMod.LOG.warn("Couldn't save atlas image", e);
+ } finally {
+ for (int i = 0; i < layerCount; i++) {
+ if (images[i] != null) {
+ images[i].close();
+ }
+ }
+
+ MemoryUtil.memFree(debugPixels);
+ }
+ });
+ }
+
+ String label(PbrMapAtlasLayer layer) {
+ return location.toString().replace(".png", "") + "_" + layer.name().toLowerCase(Locale.ROOT) + ".png";
+ }
+
+ String labelAtlas() {
+ return location.toString().replace(".png", "") + "_pbr.png";
+ }
+}
diff --git a/src/main/java/grondag/canvas/texture/pbr/PbrMapAtlasLayer.java b/src/main/java/grondag/canvas/texture/pbr/PbrMapAtlasLayer.java
new file mode 100644
index 000000000..2d54d9bb6
--- /dev/null
+++ b/src/main/java/grondag/canvas/texture/pbr/PbrMapAtlasLayer.java
@@ -0,0 +1,91 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.texture.pbr;
+
+import static grondag.canvas.texture.pbr.PbrMapSpriteLayer.AO;
+import static grondag.canvas.texture.pbr.PbrMapSpriteLayer.EMISSIVE;
+import static grondag.canvas.texture.pbr.PbrMapSpriteLayer.HEIGHT;
+import static grondag.canvas.texture.pbr.PbrMapSpriteLayer.NORMAL;
+import static grondag.canvas.texture.pbr.PbrMapSpriteLayer.REFLECTANCE;
+import static grondag.canvas.texture.pbr.PbrMapSpriteLayer.ROUGHNESS;
+
+public enum PbrMapAtlasLayer {
+ NORMAL_HEIGHT(0, new Swizzler() {
+ @Override
+ public int r(PbrMapSpriteLayer.LayeredImage image, int x, int y) {
+ return image.r(NORMAL, x, y);
+ }
+
+ @Override
+ public int g(PbrMapSpriteLayer.LayeredImage image, int x, int y) {
+ return image.g(NORMAL, x, y);
+ }
+
+ @Override
+ public int b(PbrMapSpriteLayer.LayeredImage image, int x, int y) {
+ return image.b(NORMAL, x, y);
+ }
+
+ @Override
+ public int a(PbrMapSpriteLayer.LayeredImage image, int x, int y) {
+ return image.r(HEIGHT, x, y);
+ }
+ }),
+ REFLECTANCE_ROUGHNESS_EMISSIVE_AO(1, new Swizzler() {
+ @Override
+ public int r(PbrMapSpriteLayer.LayeredImage image, int x, int y) {
+ return image.r(REFLECTANCE, x, y);
+ }
+
+ @Override
+ public int g(PbrMapSpriteLayer.LayeredImage image, int x, int y) {
+ return image.r(ROUGHNESS, x, y);
+ }
+
+ @Override
+ public int b(PbrMapSpriteLayer.LayeredImage image, int x, int y) {
+ return image.r(EMISSIVE, x, y);
+ }
+
+ @Override
+ public int a(PbrMapSpriteLayer.LayeredImage image, int x, int y) {
+ return image.r(AO, x, y);
+ }
+ });
+
+ public final int layer;
+ public final Swizzler swizzler;
+
+ PbrMapAtlasLayer(int layer, Swizzler swizzler) {
+ this.layer = layer;
+ this.swizzler = swizzler;
+ }
+
+ public interface Swizzler {
+ int r(PbrMapSpriteLayer.LayeredImage image, int x, int y);
+
+ int g(PbrMapSpriteLayer.LayeredImage image, int x, int y);
+
+ int b(PbrMapSpriteLayer.LayeredImage image, int x, int y);
+
+ int a(PbrMapSpriteLayer.LayeredImage image, int x, int y);
+ }
+}
diff --git a/src/main/java/grondag/canvas/texture/pbr/PbrMapSprite.java b/src/main/java/grondag/canvas/texture/pbr/PbrMapSprite.java
new file mode 100644
index 000000000..3841445b5
--- /dev/null
+++ b/src/main/java/grondag/canvas/texture/pbr/PbrMapSprite.java
@@ -0,0 +1,227 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.texture.pbr;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import org.lwjgl.system.MemoryUtil;
+
+import com.mojang.blaze3d.platform.TextureUtil;
+
+import net.minecraft.resources.ResourceLocation;
+
+import grondag.canvas.CanvasMod;
+import grondag.canvas.render.CanvasTextureState;
+import grondag.canvas.varia.GFX;
+
+public class PbrMapSprite implements PbrMapSpriteLayer.LayeredImage {
+ private final ResourceLocation atlasLocation;
+ private final ResourceLocation location;
+ final int x;
+ final int y;
+ final int width;
+ final int height;
+ private final Map processMap = new Object2ObjectOpenHashMap<>();
+ private final Map pixelMap = new Object2ObjectOpenHashMap<>();
+ private boolean processed = false;
+ private boolean downloaded = false;
+ private boolean closed = false;
+
+ public PbrMapSprite(ResourceLocation atlasLocation, ResourceLocation location, int x, int y, int width, int height) {
+ this.atlasLocation = atlasLocation;
+ this.location = location;
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ }
+
+ void withProcess(PbrMapSpriteLayer layer, PbrProcess process, InputTextureManager.InputTexture[] inputTexture) {
+ if (processMap.containsKey(layer)) {
+ CanvasMod.LOG.warn("Not applying duplicate PBR processor for sprite " + location + " on layer " + layer.name());
+ } else {
+ int pbrOutput = TextureUtil.generateTextureId();
+ CanvasTextureState.bindTexture(pbrOutput);
+
+ GFX.texParameter(GFX.GL_TEXTURE_2D, GFX.GL_TEXTURE_MIN_FILTER, GFX.GL_NEAREST);
+ GFX.texParameter(GFX.GL_TEXTURE_2D, GFX.GL_TEXTURE_MAG_FILTER, GFX.GL_NEAREST);
+ GFX.texParameter(GFX.GL_TEXTURE_2D, GFX.GL_TEXTURE_WRAP_S, GFX.GL_CLAMP_TO_EDGE);
+ GFX.texParameter(GFX.GL_TEXTURE_2D, GFX.GL_TEXTURE_WRAP_T, GFX.GL_CLAMP_TO_EDGE);
+
+ // if (lod > 0) {
+ // GFX.texParameter(GFX.GL_TEXTURE_2D, GFX.GL_TEXTURE_MIN_LOD, 0);
+ // GFX.texParameter(GFX.GL_TEXTURE_2D, GFX.GL_TEXTURE_MAX_LOD, lod);
+ // GFX.texParameter(GFX.GL_TEXTURE_2D, GFX.GL_TEXTURE_MAX_LEVEL, lod);
+ // GFX.texParameter(GFX.GL_TEXTURE_2D, GFX.GL_TEXTURE_LOD_BIAS, 0.0F);
+ // }
+
+ GFX.texImage2D(GFX.GL_TEXTURE_2D, 0, layer.glInternalFormat, width, height, 0, layer.glFormat, layer.glPixelDataType, (ByteBuffer) null);
+
+ processMap.put(layer, new ProcessInfo(process, inputTexture, pbrOutput));
+ }
+ }
+
+ void process() {
+ if (processed) {
+ throw new IllegalStateException("Processing PBR map twice for " + location);
+ }
+
+ for (var process : processMap.values()) {
+ process.processStatus = process.process.process(location.toString(), width, height, process.inputTexture, process.outputTexId);
+ }
+
+ processed = true;
+ }
+
+ void download() {
+ if (downloaded) {
+ throw new IllegalStateException("Downloading PBR map result twice for " + location);
+ }
+
+ for (var layer : PbrMapSpriteLayer.values()) {
+ if (processMap.containsKey(layer) && processMap.get(layer).processStatus) {
+ final ByteBuffer pixelBuffer = MemoryUtil.memAlloc(width * height * layer.bytes);
+
+ GFX.pixelStore(GFX.GL_PACK_ALIGNMENT, layer.alignment);
+
+ CanvasTextureState.bindTexture(processMap.get(layer).outputTexId);
+ GFX.glGetTexImage(GFX.GL_TEXTURE_2D, 0, layer.glFormat, layer.glPixelDataType, pixelBuffer);
+
+ pixelMap.put(layer, pixelBuffer);
+ } else {
+ // TODO: this is DEBUG, should just spit out default instead of this
+ // var input = PbrLoader.inputTextureManager.getSpriteDefault(location);
+ //
+ // if (input instanceof InputTextureManager.ImageInput imageInput) {
+ // final ByteBuffer pixelBuffer = MemoryUtil.memAlloc(width * height * 4);
+ // final var nativeImage = imageInput.image;
+ //
+ // for (int row = 0; row < height; row++) {
+ // for (int i = 0; i < width; i++) {
+ // // all of this is unnecessarily complex for the sake of consistency
+ // for (int byteOrder = 0; byteOrder < layer.bytes; byteOrder++) {
+ // byte toPut = switch (byteOrder) {
+ // case (ColorEncode.RED_BYTE_OFFSET) -> nativeImage.format().hasRed() ? nativeImage.getRedOrLuminance(i, row) : 0;
+ // case (ColorEncode.GREEN_BYTE_OFFSET) -> nativeImage.format().hasGreen() ? nativeImage.getGreenOrLuminance(i, row) : 0;
+ // case (ColorEncode.BLUE_BYTE_OFFSET) -> nativeImage.format().hasBlue() ? nativeImage.getBlueOrLuminance(i, row) : 0;
+ // case (ColorEncode.ALPHA_BYTE_OFFSET) -> nativeImage.format().hasAlpha() ? nativeImage.getLuminanceOrAlpha(i, row) : 0xF;
+ // default -> 0;
+ // };
+ //
+ // pixelBuffer.put(toPut);
+ // }
+ // }
+ // }
+ //
+ // pixelMap.put(layer, pixelBuffer);
+ // }
+ }
+ }
+
+ downloaded = true;
+ }
+
+ void close() {
+ if (closed) {
+ throw new IllegalStateException("Closing PBR sprite twice for " + location);
+ }
+
+ for (var pixel : pixelMap.values()) {
+ MemoryUtil.memFree(pixel);
+ }
+
+ for (var process : processMap.values()) {
+ TextureUtil.releaseTextureId(process.outputTexId);
+ }
+
+ processMap.clear();
+ pixelMap.clear();
+
+ closed = true;
+ }
+
+ @Override
+ public int r(PbrMapSpriteLayer layer, int x, int y) {
+ if (closed) {
+ throw new IllegalStateException("Accessed closed PBR sprite " + location);
+ }
+
+ if (pixelMap.containsKey(layer)) {
+ return pixelMap.get(layer).get((x + y * width) * layer.bytes + Math.min(layer.bytes, ColorEncode.RED_BYTE_OFFSET));
+ } else {
+ return ColorEncode.r(layer.defaultValue);
+ }
+ }
+
+ @Override
+ public int g(PbrMapSpriteLayer layer, int x, int y) {
+ if (closed) {
+ throw new IllegalStateException("Accessed closed PBR sprite " + location);
+ }
+
+ if (pixelMap.containsKey(layer)) {
+ return pixelMap.get(layer).get((x + y * width) * layer.bytes + Math.min(layer.bytes, ColorEncode.GREEN_BYTE_OFFSET));
+ } else {
+ return ColorEncode.g(layer.defaultValue);
+ }
+ }
+
+ @Override
+ public int b(PbrMapSpriteLayer layer, int x, int y) {
+ if (closed) {
+ throw new IllegalStateException("Accessed closed PBR sprite " + location);
+ }
+
+ if (pixelMap.containsKey(layer)) {
+ return pixelMap.get(layer).get((x + y * width) * layer.bytes + Math.min(layer.bytes, ColorEncode.BLUE_BYTE_OFFSET));
+ } else {
+ return ColorEncode.b(layer.defaultValue);
+ }
+ }
+
+ @Override
+ public int a(PbrMapSpriteLayer layer, int x, int y) {
+ if (closed) {
+ throw new IllegalStateException("Accessed closed PBR sprite " + location);
+ }
+
+ if (pixelMap.containsKey(layer)) {
+ return pixelMap.get(layer).get((x + y * width) * layer.bytes + Math.min(layer.bytes, ColorEncode.ALPHA_BYTE_OFFSET));
+ } else {
+ return ColorEncode.a(layer.defaultValue);
+ }
+ }
+
+ static final class ProcessInfo {
+ private final PbrProcess process;
+ private final InputTextureManager.InputTexture[] inputTexture;
+ private final int outputTexId;
+ private boolean processStatus = false;
+
+ ProcessInfo(PbrProcess process, InputTextureManager.InputTexture[] inputTexture, int outputTexId) {
+ this.process = process;
+ this.inputTexture = inputTexture;
+ this.outputTexId = outputTexId;
+ }
+ }
+}
diff --git a/src/main/java/grondag/canvas/texture/pbr/PbrMapSpriteLayer.java b/src/main/java/grondag/canvas/texture/pbr/PbrMapSpriteLayer.java
new file mode 100644
index 000000000..79b9ff612
--- /dev/null
+++ b/src/main/java/grondag/canvas/texture/pbr/PbrMapSpriteLayer.java
@@ -0,0 +1,68 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.texture.pbr;
+
+import java.nio.ByteBuffer;
+
+import grondag.canvas.varia.GFX;
+
+public enum PbrMapSpriteLayer {
+ NORMAL(ColorEncode.encode(128, 128, 255, 0), 1, 3, GFX.GL_RGB8, GFX.GL_RGB, GFX.GL_UNSIGNED_BYTE),
+ HEIGHT(ColorEncode.encode(128, 0, 0, 0), 1, 1, GFX.GL_R8, GFX.GL_RED, GFX.GL_UNSIGNED_BYTE),
+ REFLECTANCE(ColorEncode.encode(10, 0, 0, 0), 1, 1, GFX.GL_R8, GFX.GL_RED, GFX.GL_UNSIGNED_BYTE),
+ ROUGHNESS(ColorEncode.encode(255, 0, 0, 0), 1, 1, GFX.GL_R8, GFX.GL_RED, GFX.GL_UNSIGNED_BYTE),
+ EMISSIVE(ColorEncode.encode(0, 0, 0, 0), 1, 1, GFX.GL_R8, GFX.GL_RED, GFX.GL_UNSIGNED_BYTE),
+ AO(ColorEncode.encode(255, 0, 0, 0), 1, 1, GFX.GL_R8, GFX.GL_RED, GFX.GL_UNSIGNED_BYTE);
+
+ public final int defaultValue;
+ public final int alignment;
+ public final int bytes;
+ public final int glInternalFormat;
+ public final int glFormat;
+ public final int glPixelDataType;
+
+ PbrMapSpriteLayer(int defaultValue, int alignment, int bytes, int glInternalFormat, int glFormat, int glPixelDataType) {
+ this.defaultValue = defaultValue;
+ this.alignment = alignment;
+ this.bytes = bytes;
+ this.glInternalFormat = glInternalFormat;
+ this.glFormat = glFormat;
+ this.glPixelDataType = glPixelDataType;
+ }
+
+ public void drawDefault(ByteBuffer buffer) {
+ buffer.position(0);
+
+ while (buffer.position() < buffer.limit() - 4) {
+ buffer.putInt(defaultValue);
+ }
+ }
+
+ public interface LayeredImage {
+ int r(PbrMapSpriteLayer layer, int x, int y);
+
+ int g(PbrMapSpriteLayer layer, int x, int y);
+
+ int b(PbrMapSpriteLayer layer, int x, int y);
+
+ int a(PbrMapSpriteLayer layer, int x, int y);
+ }
+}
diff --git a/src/main/java/grondag/canvas/texture/pbr/PbrProcess.java b/src/main/java/grondag/canvas/texture/pbr/PbrProcess.java
new file mode 100644
index 000000000..369e35153
--- /dev/null
+++ b/src/main/java/grondag/canvas/texture/pbr/PbrProcess.java
@@ -0,0 +1,151 @@
+/*
+ * This file is part of Canvas Renderer and is licensed to the project under
+ * terms that are compatible with the GNU Lesser General Public License.
+ * See the NOTICE file distributed with this work for additional information
+ * regarding copyright ownership and licensing.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package grondag.canvas.texture.pbr;
+
+import org.joml.Matrix4f;
+
+import net.minecraft.resources.ResourceLocation;
+
+import grondag.canvas.CanvasMod;
+import grondag.canvas.buffer.format.CanvasVertexFormats;
+import grondag.canvas.buffer.input.DrawableVertexCollector;
+import grondag.canvas.buffer.input.SimpleVertexCollector;
+import grondag.canvas.buffer.render.StaticDrawBuffer;
+import grondag.canvas.buffer.render.TransferBuffer;
+import grondag.canvas.buffer.render.TransferBuffers;
+import grondag.canvas.material.state.RenderState;
+import grondag.canvas.pipeline.GlSymbolLookup;
+import grondag.canvas.render.CanvasTextureState;
+import grondag.canvas.shader.ProcessProgram;
+import grondag.canvas.varia.GFX;
+
+public class PbrProcess {
+ private static final String[] SAMPLERS;
+ private static final int INPUT_COUNT = 8;
+ private static int prevWidth = -1;
+ private static int prevHeight = -1;
+ private static int lastProgram = 0;
+
+ static {
+ SAMPLERS = new String[INPUT_COUNT];
+
+ for (int i = 0; i < INPUT_COUNT; i++) {
+ SAMPLERS[i] = "frx_source" + i;
+ }
+ }
+
+ private final ProcessProgram program;
+
+ public PbrProcess(ResourceLocation fragSource) {
+ this.program = new ProcessProgram(fragSource.toString(), new ResourceLocation("canvas:shaders/internal/pbr.vert"), fragSource, SAMPLERS);
+ }
+
+ public PbrProcess() {
+ this.program = new ProcessProgram("pbr_process", new ResourceLocation("canvas:shaders/internal/pbr.vert"), new ResourceLocation("frex:shaders/func/color.frag"), SAMPLERS);
+ }
+
+ boolean process(String debugLabel, int width, int height, InputTextureManager.InputTexture[] inputTexture, int outputTextureId) {
+ if (inputTexture.length > INPUT_COUNT - 1) {
+ throw new IllegalStateException("PBR process is made to accept more than " + INPUT_COUNT + " image sources");
+ }
+
+ GFX.glFramebufferTexture2D(GFX.GL_FRAMEBUFFER, GFX.GL_COLOR_ATTACHMENT0, GFX.GL_TEXTURE_2D, outputTextureId, 0);
+ final int check = GFX.checkFramebufferStatus(GFX.GL_FRAMEBUFFER);
+
+ if (check != GFX.GL_FRAMEBUFFER_COMPLETE) {
+ CanvasMod.LOG.warn("Failed PBR processing: Framebuffer " + debugLabel + " has invalid status " + check + " " + GlSymbolLookup.reverseLookup(check));
+ return false;
+ }
+
+ final boolean resized = width != prevWidth || height != prevHeight;
+
+ prevWidth = width;
+ prevHeight = height;
+
+ if (resized) {
+ GFX.viewport(0, 0, width, height);
+ }
+
+ for (int i = 0; i < inputTexture.length; ++i) {
+ CanvasTextureState.ensureTextureOfTextureUnit(GFX.GL_TEXTURE0 + i, GFX.GL_TEXTURE_2D, inputTexture[i].getTexId());
+ }
+
+ program.activate();
+ program.lod(0).layer(0).size(width, height);
+
+ final boolean programChanged = lastProgram != program.programId();
+
+ lastProgram = program.programId();
+
+ if (resized || programChanged) {
+ program.projection(new Matrix4f().setOrtho(0, width, height, 0, 1000.0F, 3000.0F));
+ }
+
+ GFX.drawArrays(GFX.GL_TRIANGLES, 0, 6);
+
+ return true;
+ }
+
+ void close() {
+ program.unload();
+ }
+
+ static class DrawContext {
+ final StaticDrawBuffer drawBuffer;
+ final int fboGlId;
+
+ {
+ final DrawableVertexCollector collector = new SimpleVertexCollector(RenderState.missing(), new int[64]);
+ final int[] v = collector.target();
+ addVertex(0f, 0f, 0.2f, 0f, 1f, v, 0);
+ addVertex(1f, 0f, 0.2f, 1f, 1f, v, 5);
+ addVertex(1f, 1f, 0.2f, 1f, 0f, v, 10);
+ addVertex(1f, 1f, 0.2f, 1f, 0f, v, 15);
+ addVertex(0f, 1f, 0.2f, 0f, 0f, v, 20);
+ addVertex(0f, 0f, 0.2f, 0f, 1f, v, 25);
+ collector.commit(30);
+
+ final TransferBuffer transfer = TransferBuffers.claim(collector.byteSize());
+ collector.toBuffer(transfer, 0);
+ drawBuffer = new StaticDrawBuffer(CanvasVertexFormats.PROCESS_VERTEX_UV, transfer);
+ drawBuffer.upload();
+ collector.clear();
+
+ fboGlId = GFX.genFramebuffer();
+ GFX.bindFramebuffer(GFX.GL_FRAMEBUFFER, fboGlId);
+ GFX.objectLabel(GFX.GL_FRAMEBUFFER, fboGlId, "FBO pbr_context");
+ GFX.glDrawBuffers(new int[]{GFX.GL_COLOR_ATTACHMENT0});
+ }
+
+ void close() {
+ drawBuffer.release();
+ GFX.deleteFramebuffer(fboGlId);
+ }
+
+ private static void addVertex(float x, float y, float z, float u, float v, int[] target, int index) {
+ target[index] = Float.floatToRawIntBits(x);
+ target[++index] = Float.floatToRawIntBits(y);
+ target[++index] = Float.floatToRawIntBits(z);
+ target[++index] = Float.floatToRawIntBits(u);
+ target[++index] = Float.floatToRawIntBits(v);
+ }
+ }
+}
diff --git a/src/main/resources/assets/canvas/shaders/internal/pbr.vert b/src/main/resources/assets/canvas/shaders/internal/pbr.vert
new file mode 100644
index 000000000..2eb5e3dc9
--- /dev/null
+++ b/src/main/resources/assets/canvas/shaders/internal/pbr.vert
@@ -0,0 +1,19 @@
+#include frex:shaders/api/header.glsl
+
+/******************************************************
+ canvas:shaders/pipeline/internal/pbr.vert
+******************************************************/
+
+uniform mat4 frxu_frameProjectionMatrix;
+uniform ivec2 frxu_size;
+
+in vec3 in_vertex;
+in vec2 in_uv;
+
+out vec2 frx_texcoord;
+
+void main() {
+ vec4 outPos = frxu_frameProjectionMatrix * vec4(in_vertex.xy * frxu_size, 0.0, 1.0);
+ gl_Position = vec4(outPos.xy, 0.2, 1.0);
+ frx_texcoord = in_uv;
+}
diff --git a/src/main/resources/assets/canvas/shaders/pipeline/post/copy.frag b/src/main/resources/assets/canvas/shaders/pipeline/post/copy.frag
index efe607be3..cbfa1fa85 100644
--- a/src/main/resources/assets/canvas/shaders/pipeline/post/copy.frag
+++ b/src/main/resources/assets/canvas/shaders/pipeline/post/copy.frag
@@ -2,7 +2,7 @@
#include canvas:shaders/pipeline/pipeline.glsl
/******************************************************
- canvas:shaders/pipeline/post/copy.frag
+ canvas:shaders/pipeline/post/color.frag
******************************************************/
uniform sampler2D _cvu_input;
diff --git a/src/main/resources/assets/frex/shaders/func/alpha.frag b/src/main/resources/assets/frex/shaders/func/alpha.frag
new file mode 100644
index 000000000..ba6a83bb8
--- /dev/null
+++ b/src/main/resources/assets/frex/shaders/func/alpha.frag
@@ -0,0 +1,15 @@
+#include frex:shaders/api/header.glsl
+
+/******************************************************
+ frex:shaders/func/alpha.frag
+******************************************************/
+
+uniform sampler2D frxs_source0;
+
+in vec2 frx_texcoord;
+
+out vec4 outColor;
+
+void main() {
+ outColor = vec4(texture(frxs_source0, frx_texcoord).a);
+}
diff --git a/src/main/resources/assets/frex/shaders/func/blue.frag b/src/main/resources/assets/frex/shaders/func/blue.frag
new file mode 100644
index 000000000..8e31d3866
--- /dev/null
+++ b/src/main/resources/assets/frex/shaders/func/blue.frag
@@ -0,0 +1,15 @@
+#include frex:shaders/api/header.glsl
+
+/******************************************************
+ frex:shaders/func/blue.frag
+******************************************************/
+
+uniform sampler2D frxs_source0;
+
+in vec2 frx_texcoord;
+
+out vec4 outColor;
+
+void main() {
+ outColor = vec4(texture(frxs_source0, frx_texcoord).b);
+}
diff --git a/src/main/resources/assets/frex/shaders/func/color.frag b/src/main/resources/assets/frex/shaders/func/color.frag
new file mode 100644
index 000000000..4b5702e37
--- /dev/null
+++ b/src/main/resources/assets/frex/shaders/func/color.frag
@@ -0,0 +1,15 @@
+#include frex:shaders/api/header.glsl
+
+/******************************************************
+ frex:shaders/func/color.frag
+******************************************************/
+
+uniform sampler2D frxs_source0;
+
+in vec2 frx_texcoord;
+
+out vec4 outColor;
+
+void main() {
+ outColor = texture(frxs_source0, frx_texcoord);
+}
diff --git a/src/main/resources/assets/frex/shaders/func/green.frag b/src/main/resources/assets/frex/shaders/func/green.frag
new file mode 100644
index 000000000..7a1b96371
--- /dev/null
+++ b/src/main/resources/assets/frex/shaders/func/green.frag
@@ -0,0 +1,15 @@
+#include frex:shaders/api/header.glsl
+
+/******************************************************
+ frex:shaders/func/green.frag
+******************************************************/
+
+uniform sampler2D frxs_source0;
+
+in vec2 frx_texcoord;
+
+out vec4 outColor;
+
+void main() {
+ outColor = vec4(texture(frxs_source0, frx_texcoord).g);
+}
diff --git a/src/main/resources/assets/frex/shaders/func/luminance.frag b/src/main/resources/assets/frex/shaders/func/luminance.frag
new file mode 100644
index 000000000..1313b27b4
--- /dev/null
+++ b/src/main/resources/assets/frex/shaders/func/luminance.frag
@@ -0,0 +1,17 @@
+#include frex:shaders/api/header.glsl
+#include frex:shaders/lib/math.glsl
+
+/******************************************************
+ frex:shaders/func/luminance.frag
+******************************************************/
+
+uniform sampler2D frxs_source0;
+
+in vec2 frx_texcoord;
+
+out vec4 outColor;
+
+void main() {
+ float luminance = frx_luminance(texture(frxs_source0, frx_texcoord).rgb);
+ outColor = vec4(luminance);
+}
diff --git a/src/main/resources/assets/frex/shaders/func/normal_from_luminance.frag b/src/main/resources/assets/frex/shaders/func/normal_from_luminance.frag
new file mode 100644
index 000000000..6b97bf019
--- /dev/null
+++ b/src/main/resources/assets/frex/shaders/func/normal_from_luminance.frag
@@ -0,0 +1,18 @@
+#include frex:shaders/api/header.glsl
+#include frex:shaders/lib/math.glsl
+
+/******************************************************
+ frex:shaders/func/normal_from_luminance.frag
+******************************************************/
+
+uniform sampler2D frxs_source0;
+uniform ivec2 frxu_size;
+
+in vec2 frx_texcoord;
+
+out vec4 outColor;
+
+void main() {
+ float luminance = frx_luminance(texture(frxs_source0, frx_texcoord).rgb);
+ outColor = vec4(normalize(vec3(dFdx(luminance), dFdy(luminance), luminance)) * 0.5 + 0.5, 1.0);
+}
diff --git a/src/main/resources/assets/frex/shaders/func/one.frag b/src/main/resources/assets/frex/shaders/func/one.frag
new file mode 100644
index 000000000..11b71b02b
--- /dev/null
+++ b/src/main/resources/assets/frex/shaders/func/one.frag
@@ -0,0 +1,11 @@
+#include frex:shaders/api/header.glsl
+
+/******************************************************
+ frex:shaders/func/one.frag
+******************************************************/
+
+out vec4 outColor;
+
+void main() {
+ outColor = vec4(1.0);
+}
diff --git a/src/main/resources/assets/frex/shaders/func/red.frag b/src/main/resources/assets/frex/shaders/func/red.frag
new file mode 100644
index 000000000..26b1e9285
--- /dev/null
+++ b/src/main/resources/assets/frex/shaders/func/red.frag
@@ -0,0 +1,15 @@
+#include frex:shaders/api/header.glsl
+
+/******************************************************
+ frex:shaders/func/red.frag
+******************************************************/
+
+uniform sampler2D frxs_source0;
+
+in vec2 frx_texcoord;
+
+out vec4 outColor;
+
+void main() {
+ outColor = vec4(texture(frxs_source0, frx_texcoord).r);
+}
diff --git a/src/main/resources/assets/frex/shaders/func/zero.frag b/src/main/resources/assets/frex/shaders/func/zero.frag
new file mode 100644
index 000000000..1f6d27d28
--- /dev/null
+++ b/src/main/resources/assets/frex/shaders/func/zero.frag
@@ -0,0 +1,11 @@
+#include frex:shaders/api/header.glsl
+
+/******************************************************
+ frex:shaders/func/zero.frag
+******************************************************/
+
+out vec4 outColor;
+
+void main() {
+ outColor = vec4(0.0);
+}
diff --git a/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/post/copy.frag b/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/post/copy.frag
index e1c47e073..b089aab2a 100644
--- a/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/post/copy.frag
+++ b/src/main/resources/resourcepacks/abstract/assets/abstract/shaders/pipeline/post/copy.frag
@@ -2,7 +2,7 @@
#include abstract:shaders/pipeline/pipeline.glsl
/******************************************************
- abstract:shaders/pipeline/post/copy.frag
+ abstract:shaders/pipeline/post/color.frag
******************************************************/
uniform sampler2D _cvu_input;