Skip to content

Shader Integration Example

This comprehensive tutorial shows how to create advanced shader integration systems, including post-processing chains, custom uniform management, and multi-pass rendering effects.

Before starting this tutorial, you should understand:

  • Basic GLSL shader programming
  • Minecraft’s render system architecture
  • Buffer and texture management

Let’s create a post-processing system similar to Minecraft’s PostChain:

// Example: Custom post-processing manager
public class PostProcessingManager {
private final Map<String, PostProcessingEffect> effects = new HashMap<>();
private final List<PostProcessingPass> activePasses = new ArrayList<>();
private final Map<String, RenderTarget> targets = new HashMap<>();
private final FrameBuffer mainFrameBuffer;
public PostProcessingManager(int width, int height) {
this.mainFrameBuffer = new FrameBuffer(width, height, true);
initializeTargets(width, height);
loadEffects();
}
private void initializeTargets(int width, int height) {
// Create render targets for different purposes
targets.put("scene", createRenderTarget(width, height, true));
targets.put("bloom", createRenderTarget(width / 2, height / 2, true));
targets.put("blur_h", createRenderTarget(width / 4, height / 4, true));
targets.put("blur_v", createRenderTarget(width / 4, height / 4, true));
targets.put("final", createRenderTarget(width, height, true));
}
private RenderTarget createRenderTarget(int width, int height, boolean hasDepth) {
RenderTarget target = new RenderTarget(width, height, hasDepth);
target.setClearColor(0, 0, 0, 0);
return target;
}
private void loadEffects() {
effects.put("bloom", new BloomEffect());
effects.put("motion_blur", new MotionBlurEffect());
effects.put("color_correction", new ColorCorrectionEffect());
effects.put("vignette", new VignetteEffect());
}
public void addEffect(String effectName, float intensity) {
PostProcessingEffect effect = effects.get(effectName);
if (effect != null) {
effect.setIntensity(intensity);
activePasses.addAll(effect.getPasses());
}
}
public void process(float partialTick, PoseStack poseStack) {
if (activePasses.isEmpty()) return;
// Capture scene to main framebuffer
mainFrameBuffer.bind();
renderScene(poseStack);
mainFrameBuffer.unbind();
// Process all post-processing passes
for (PostProcessingPass pass : activePasses) {
pass.process(partialTick, this);
}
// Blit final result to screen
RenderTarget finalTarget = targets.get("final");
if (finalTarget != null) {
blitToScreen(finalTarget);
}
}
public RenderTarget getTarget(String name) {
return targets.get(name);
}
public void resize(int width, int height) {
// Resize all render targets
targets.values().forEach(target -> target.resize(width, height));
}
}
// Example: Base class for post-processing passes
public abstract class PostProcessingPass {
protected final String name;
protected final ResourceLocation shaderLocation;
protected ShaderInstance shader;
protected float intensity = 1.0f;
public PostProcessingPass(String name, String shaderName) {
this.name = name;
this.shaderLocation = new ResourceLocation("modid", shaderName);
}
public void initialize(ResourceProvider resourceProvider) {
try {
this.shader = new ShaderInstance(resourceProvider, shaderLocation,
DefaultVertexFormat.POSITION_TEX_COLOR);
setupUniforms();
} catch (IOException e) {
throw new RuntimeException("Failed to load shader: " + shaderLocation, e);
}
}
protected abstract void setupUniforms();
public abstract void process(float partialTick, PostProcessingManager manager);
public void setIntensity(float intensity) {
this.intensity = intensity;
}
protected void bindInputTextures(String... textureNames) {
for (int i = 0; i < textureNames.length; i++) {
RenderTarget target = manager.getTarget(textureNames[i]);
if (target != null) {
RenderSystem.setShaderTexture(i, target.getColorTextureId());
}
}
}
protected void setCommonUniforms(float partialTick) {
if (shader != null) {
shader.safeGetUniform("Intensity").set(intensity);
shader.safeGetUniform("Time").set(Minecraft.getInstance().level.getGameTime() + partialTick);
shader.safeGetUniform("Resolution").set(
new Vector2f(Minecraft.getInstance().getWindow().getGuiScaledWidth(),
Minecraft.getInstance().getWindow().getGuiScaledHeight())
);
}
}
protected void renderFullscreenQuad() {
RenderSystem.setShader(() -> shader);
Tesselator tesselator = Tesselator.getInstance();
BufferBuilder buffer = tesselator.getBuilder();
buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR);
// Fullscreen quad vertices
buffer.vertex(-1.0f, -1.0f, 0.0f).uv(0.0f, 0.0f).color(1.0f, 1.0f, 1.0f, 1.0f).endVertex();
buffer.vertex(1.0f, -1.0f, 0.0f).uv(1.0f, 0.0f).color(1.0f, 1.0f, 1.0f, 1.0f).endVertex();
buffer.vertex(1.0f, 1.0f, 0.0f).uv(1.0f, 1.0f).color(1.0f, 1.0f, 1.0f, 1.0f).endVertex();
buffer.vertex(-1.0f, 1.0f, 0.0f).uv(0.0f, 1.0f).color(1.0f, 1.0f, 1.0f, 1.0f).endVertex();
tesselator.end();
}
}
// Example: Bloom effect implementation
public class BloomEffect extends PostProcessingEffect {
public BloomEffect() {
super("bloom");
}
@Override
public List<PostProcessingPass> getPasses() {
return Arrays.asList(
new BrightnessPass(),
new GaussianBlurHorizontalPass(),
new GaussianBlurVerticalPass(),
new BloomCombinePass()
);
}
private static class BrightnessPass extends PostProcessingPass {
private UniformFloat threshold;
private UniformFloat softThreshold;
public BrightnessPass() {
super("brightness", "brightness");
}
@Override
protected void setupUniforms() {
threshold = shader.safeGetUniform("Threshold");
softThreshold = shader.safeGetUniform("SoftThreshold");
}
@Override
public void process(float partialTick, PostProcessingManager manager) {
RenderTarget sceneTarget = manager.getTarget("scene");
RenderTarget bloomTarget = manager.getTarget("bloom");
if (sceneTarget != null && bloomTarget != null) {
bloomTarget.bind();
bindInputTextures("scene");
setCommonUniforms(partialTick);
threshold.set(1.0f);
softThreshold.set(0.5f);
renderFullscreenQuad();
bloomTarget.unbind();
}
}
}
private static class GaussianBlurHorizontalPass extends PostProcessingPass {
private UniformFloat texelSize;
public GaussianBlurHorizontalPass() {
super("blur_h", "gaussian_blur");
}
@Override
protected void setupUniforms() {
texelSize = shader.safeGetUniform("TexelSize");
}
@Override
public void process(float partialTick, PostProcessingManager manager) {
RenderTarget bloomTarget = manager.getTarget("bloom");
RenderTarget blurHTarget = manager.getTarget("blur_h");
if (bloomTarget != null && blurHTarget != null) {
blurHTarget.bind();
bindInputTextures("bloom");
setCommonUniforms(partialTick);
// Set texel size for horizontal blur
int width = bloomTarget.width;
texelSize.set(1.0f / width);
// Set blur direction (horizontal)
shader.safeGetUniform("Direction").set(new Vector2f(1.0f, 0.0f));
renderFullscreenQuad();
blurHTarget.unbind();
}
}
}
private static class GaussianBlurVerticalPass extends PostProcessingPass {
private UniformFloat texelSize;
public GaussianBlurVerticalPass() {
super("blur_v", "gaussian_blur");
}
@Override
protected void setupUniforms() {
texelSize = shader.safeGetUniform("TexelSize");
}
@Override
public void process(float partialTick, PostProcessingManager manager) {
RenderTarget blurHTarget = manager.getTarget("blur_h");
RenderTarget blurVTarget = manager.getTarget("blur_v");
if (blurHTarget != null && blurVTarget != null) {
blurVTarget.bind();
bindInputTextures("blur_h");
setCommonUniforms(partialTick);
// Set texel size for vertical blur
int height = blurHTarget.height;
texelSize.set(1.0f / height);
// Set blur direction (vertical)
shader.safeGetUniform("Direction").set(new Vector2f(0.0f, 1.0f));
renderFullscreenQuad();
blurVTarget.unbind();
}
}
}
private static class BloomCombinePass extends PostProcessingPass {
public BloomCombinePass() {
super("bloom_combine", "bloom_combine");
}
@Override
protected void setupUniforms() {
// Setup bloom combine specific uniforms
}
@Override
public void process(float partialTick, PostProcessingManager manager) {
RenderTarget sceneTarget = manager.getTarget("scene");
RenderTarget blurVTarget = manager.getTarget("blur_v");
RenderTarget finalTarget = manager.getTarget("final");
if (sceneTarget != null && blurVTarget != null && finalTarget != null) {
finalTarget.bind();
// Bind both scene and bloom textures
RenderSystem.setShaderTexture(0, sceneTarget.getColorTextureId());
RenderSystem.setShaderTexture(1, blurVTarget.getColorTextureId());
setCommonUniforms(partialTick);
renderFullscreenQuad();
finalTarget.unbind();
}
}
}
}
// Example: Advanced shader management system
public class AdvancedShaderManager {
private final Map<String, ShaderProgram> loadedShaders = new ConcurrentHashMap<>();
private final Map<String, ShaderUniforms> uniformCache = new ConcurrentHashMap<>();
private final ResourceProvider resourceProvider;
private final ShaderHotReloader hotReloader;
public AdvancedShaderManager(ResourceProvider resourceProvider) {
this.resourceProvider = resourceProvider;
this.hotReloader = new ShaderHotReloader(this);
}
public ShaderProgram getShader(String shaderName) {
return loadedShaders.computeIfAbsent(shaderName, this::loadShader);
}
private ShaderProgram loadShader(String shaderName) {
try {
String vertexSource = loadShaderSource(shaderName + ".vert");
String fragmentSource = loadShaderSource(shaderName + ".frag");
ShaderProgram program = new ShaderProgram(vertexSource, fragmentSource);
program.compile();
// Cache uniform locations
uniformCache.put(shaderName, new ShaderUniforms(program));
return program;
} catch (Exception e) {
throw new RuntimeException("Failed to load shader: " + shaderName, e);
}
}
private String loadShaderSource(String filename) throws IOException {
ResourceLocation location = new ResourceLocation("modid", "shaders/" + filename);
try (InputStream stream = resourceProvider.getResource(location).getInputStream()) {
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
}
public void reloadShader(String shaderName) {
ShaderProgram oldProgram = loadedShaders.get(shaderName);
if (oldProgram != null) {
oldProgram.cleanup();
}
loadedShaders.remove(shaderName);
uniformCache.remove(shaderName);
// Force reload
getShader(shaderName);
}
public void enableHotReload() {
hotReloader.start();
}
public void disableHotReload() {
hotReloader.stop();
}
}
// Example: Custom shader program with advanced features
public class ShaderProgram {
private int programId;
private int vertexShaderId;
private int fragmentShaderId;
private boolean compiled = false;
private final Map<String, Integer> uniformLocations = new HashMap<>();
public ShaderProgram(String vertexSource, String fragmentSource) {
this.vertexShaderId = compileShader(vertexSource, GL20.GL_VERTEX_SHADER);
this.fragmentShaderId = compileShader(fragmentSource, GL20.GL_FRAGMENT_SHADER);
this.programId = GL20.glCreateProgram();
GL20.glAttachShader(programId, vertexShaderId);
GL20.glAttachShader(programId, fragmentShaderId);
}
private int compileShader(String source, int type) {
int shaderId = GL20.glCreateShader(type);
GL20.glShaderSource(shaderId, source);
GL20.glCompileShader(shaderId);
// Check compilation status
if (GL20.glGetShaderi(shaderId, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
String log = GL20.glGetShaderInfoLog(shaderId);
throw new RuntimeException("Shader compilation failed: " + log);
}
return shaderId;
}
public void compile() {
if (compiled) return;
GL20.glLinkProgram(programId);
// Check linking status
if (GL20.glGetProgrami(programId, GL20.GL_LINK_STATUS) == GL11.GL_FALSE) {
String log = GL20.glGetProgramInfoLog(programId);
throw new RuntimeException("Shader linking failed: " + log);
}
// Validate program
GL20.glValidateProgram(programId);
if (GL20.glGetProgrami(programId, GL20.GL_VALIDATE_STATUS) == GL11.GL_FALSE) {
String log = GL20.glGetProgramInfoLog(programId);
throw new RuntimeException("Shader validation failed: " + log);
}
compiled = true;
}
public void use() {
if (!compiled) {
compile();
}
GL20.glUseProgram(programId);
}
public void setUniform(String name, float value) {
int location = getUniformLocation(name);
GL20.glUniform1f(location, value);
}
public void setUniform(String name, Vector2f value) {
int location = getUniformLocation(name);
GL20.glUniform2f(location, value.x, value.y);
}
public void setUniform(String name, Vector3f value) {
int location = getUniformLocation(name);
GL20.glUniform3f(location, value.x, value.y, value.z);
}
public void setUniform(String name, Vector4f value) {
int location = getUniformLocation(name);
GL20.glUniform4f(location, value.x, value.y, value.z, value.w);
}
public void setUniform(String name, Matrix4f value) {
int location = getUniformLocation(name);
// Convert to float array
float[] matrix = new float[16];
value.get(matrix);
GL20.glUniformMatrix4fv(location, false, matrix);
}
public void setUniform(String name, int textureUnit) {
int location = getUniformLocation(name);
GL20.glUniform1i(location, textureUnit);
}
private int getUniformLocation(String name) {
return uniformLocations.computeIfAbsent(name, n -> GL20.glGetUniformLocation(programId, n));
}
public void cleanup() {
GL20.glDeleteShader(vertexShaderId);
GL20.glDeleteShader(fragmentShaderId);
GL20.glDeleteProgram(programId);
}
}

Brightness Filter Shader (brightness.frag)

Section titled “Brightness Filter Shader (brightness.frag)”
#version 150
uniform sampler2D u_scene;
uniform float u_intensity;
uniform float u_threshold;
uniform float u_softThreshold;
in vec2 v_texCoord;
out vec4 fragColor;
void main() {
vec4 color = texture(u_scene, v_texCoord);
// Calculate brightness
float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
// Apply soft threshold
float knee = u_threshold * u_softThreshold;
float soft = brightness - knee + knee * knee / (brightness + 0.0001);
soft = clamp(soft, 0.0, 1.0);
// Combine with original brightness
float contribution = max(soft, brightness - u_threshold);
// Apply intensity
contribution *= u_intensity;
fragColor = vec4(color.rgb * contribution, color.a);
}
#version 150
uniform sampler2D u_texture;
uniform vec2 u_resolution;
uniform vec2 u_direction;
uniform float u_intensity;
in vec2 v_texCoord;
out vec4 fragColor;
// Gaussian weights for 9-tap blur
const float weights[9] = float[](
1.0/16.0, 2.0/16.0, 1.0/16.0,
2.0/16.0, 4.0/16.0, 2.0/16.0,
1.0/16.0, 2.0/16.0, 1.0/16.0
);
void main() {
vec2 texelSize = 1.0 / u_resolution;
vec4 color = vec4(0.0);
// 3x3 Gaussian blur
for (int x = 0; x < 3; x++) {
for (int y = 0; y < 3; y++) {
vec2 offset = vec2(float(x - 1), float(y - 1)) * texelSize * u_direction;
vec2 sampleCoord = v_texCoord + offset;
float weight = weights[x * 3 + y];
color += texture(u_texture, sampleCoord) * weight;
}
}
fragColor = color * u_intensity;
}
#version 150
uniform sampler2D u_scene;
uniform sampler2D u_bloom;
uniform float u_intensity;
uniform float u_saturation;
in vec2 v_texCoord;
out vec4 fragColor;
// Color saturation helper
vec3 saturate(vec3 color, float amount) {
vec3 luminance = vec3(0.2126, 0.7152, 0.0722);
vec3 gray = vec3(dot(color, luminance));
return mix(gray, color, amount);
}
void main() {
vec4 sceneColor = texture(u_scene, v_texCoord);
vec4 bloomColor = texture(u_bloom, v_texCoord);
// Saturate bloom
bloomColor.rgb = saturate(bloomColor.rgb, u_saturation);
// Combine scene and bloom
vec4 finalColor = sceneColor + bloomColor * u_intensity;
fragColor = finalColor;
}
// Example: Shader hot reloader for development
public class ShaderHotReloader {
private final AdvancedShaderManager manager;
private final Map<String, Long> lastModified = new HashMap<>();
private final ScheduledExecutorService scheduler;
private boolean running = false;
public ShaderHotReloader(AdvancedShaderManager manager) {
this.manager = manager;
this.scheduler = Executors.newSingleThreadScheduledExecutor();
}
public void start() {
if (running) return;
running = true;
scheduler.scheduleAtFixedRate(this::checkForChanges, 1, 1, TimeUnit.SECONDS);
}
public void stop() {
running = false;
scheduler.shutdown();
}
private void checkForChanges() {
try {
Path shaderDir = Paths.get("assets/modid/shaders");
if (!Files.exists(shaderDir)) return;
Files.walk(shaderDir)
.filter(path -> path.toString().endsWith(".vert") || path.toString().endsWith(".frag"))
.forEach(this::checkFile);
} catch (IOException e) {
System.err.println("Error checking shader files: " + e.getMessage());
}
}
private void checkFile(Path file) {
try {
long lastModified = Files.getLastModifiedTime(file).toMillis();
String fileName = file.getFileName().toString();
String shaderName = fileName.substring(0, fileName.lastIndexOf('.'));
Long lastSeen = lastModified.get(shaderName);
if (lastSeen == null || lastModified > lastSeen) {
// File has been modified
System.out.println("Detected change in shader: " + shaderName);
// Find corresponding shader files
String vertexName = findShaderFile(shaderName, ".vert");
String fragmentName = findShaderFile(shaderName, ".frag");
if (vertexName != null && fragmentName != null) {
try {
manager.reloadShader(vertexName.replace(".vert", ""));
System.out.println("Successfully reloaded shader: " + vertexName);
} catch (Exception e) {
System.err.println("Failed to reload shader: " + e.getMessage());
}
}
lastModified.put(shaderName, lastModified);
}
} catch (IOException e) {
System.err.println("Error checking file: " + e.getMessage());
}
}
private String findShaderFile(String baseName, String extension) {
// Find matching shader file
return baseName + extension; // Simplified for example
}
}
// Example: Integrate with game rendering
public class GameRendererMod {
private final PostProcessingManager postProcessingManager;
private final AdvancedShaderManager shaderManager;
private boolean postProcessingEnabled = true;
public GameRendererMod() {
this.shaderManager = new AdvancedShaderManager(Minecraft.getInstance().getResourceManager());
this.postProcessingManager = new PostProcessingManager(
Minecraft.getInstance().getWindow().getWidth(),
Minecraft.getInstance().getWindow().getHeight()
);
initializeEffects();
shaderManager.enableHotReload(); // Enable for development
}
private void initializeEffects() {
// Enable bloom effect with default intensity
postProcessingManager.addEffect("bloom", 0.5f);
// Add color correction
postProcessingManager.addEffect("color_correction", 0.3f);
}
public void render(float partialTick) {
if (postProcessingEnabled) {
postProcessingManager.process(partialTick, new PoseStack());
} else {
// Default rendering without post-processing
renderScene(new PoseStack());
}
}
public void onResolutionChanged(int width, int height) {
postProcessingManager.resize(width, height);
}
public void togglePostProcessing() {
postProcessingEnabled = !postProcessingEnabled;
}
public void setBloomIntensity(float intensity) {
postProcessingManager.addEffect("bloom", intensity);
}
public void cleanup() {
shaderManager.disableHotReload();
// Cleanup resources...
}
}
// Example: Shader debugging utilities
public class ShaderDebugger {
private final AdvancedShaderManager shaderManager;
private boolean debugMode = false;
public ShaderDebugger(AdvancedShaderManager manager) {
this.shaderManager = manager;
}
public void enableDebugMode() {
debugMode = true;
System.out.println("Shader debug mode enabled");
}
public void renderDebugInfo(PoseStack poseStack) {
if (!debugMode) return;
// Render debug information about shaders
renderShaderInfo(poseStack);
}
private void renderShaderInfo(PoseStack poseStack) {
// Render current shader information
ShaderProgram currentShader = getCurrentShader();
if (currentShader != null) {
// Display shader uniform values
displayUniformValues(currentShader);
}
// Render render target previews
renderRenderTargetPreviews(poseStack);
}
private void displayUniformValues(ShaderProgram shader) {
System.out.println("=== Shader Uniforms ===");
// Display current uniform values for debugging
}
private void renderRenderTargetPreviews(PoseStack poseStack) {
// Render small previews of render targets
// This helps visualize intermediate post-processing results
}
public void validateShader(String shaderName) {
try {
ShaderProgram program = shaderManager.getShader(shaderName);
// Test compilation
program.compile();
System.out.println("Shader " + shaderName + " compiled successfully");
// Validate uniforms
validateUniforms(program);
} catch (Exception e) {
System.err.println("Shader validation failed: " + shaderName);
System.err.println("Error: " + e.getMessage());
}
}
private void validateUniforms(ShaderProgram program) {
// Check that all expected uniforms are present
// Log missing or invalid uniforms
}
}
// Example: Shader performance optimization
public class ShaderOptimizer {
private final Map<String, OptimizedShader> optimizedShaders = new HashMap<>();
public OptimizedShader getOptimizedShader(String name, String vertexSource, String fragmentSource) {
return optimizedShaders.computeIfAbsent(name, n -> optimizeShader(vertexSource, fragmentSource));
}
private OptimizedShader optimizeShader(String vertexSource, String fragmentSource) {
// Apply optimizations
String optimizedVertex = optimizeVertexShader(vertexSource);
String optimizedFragment = optimizeFragmentShader(fragmentSource);
return new OptimizedShader(optimizedVertex, optimizedFragment);
}
private String optimizeVertexShader(String source) {
// Remove unused variables
source = removeUnusedVariables(source);
// Optimize arithmetic operations
source = optimizeArithmetic(source);
// Inline constant expressions
source = inlineConstants(source);
return source;
}
private String optimizeFragmentShader(String source) {
// Similar optimizations for fragment shader
source = removeUnusedVariables(source);
source = optimizeTextureLookups(source);
source = optimizeMathFunctions(source);
return source;
}
private String removeUnusedVariables(String source) {
// Analysis and removal of unused variables
// This is a simplified example
return source;
}
private String optimizeArithmetic(String source) {
// Optimize arithmetic operations
// E.g., replace x * 2.0 with x + x, etc.
return source;
}
private String inlineConstants(String source) {
// Inline constant expressions
return source;
}
private String optimizeTextureLookups(String source) {
// Optimize texture sampling operations
return source;
}
private String optimizeMathFunctions(String source) {
// Replace expensive functions with faster alternatives where possible
return source;
}
private static class OptimizedShader {
private final String vertexSource;
private final String fragmentSource;
private final ShaderProgram compiledProgram;
public OptimizedShader(String vertexSource, String fragmentSource) {
this.vertexSource = vertexSource;
this.fragmentSource = fragmentSource;
this.compiledProgram = new ShaderProgram(vertexSource, fragmentSource);
this.compiledProgram.compile();
}
public void use() {
compiledProgram.use();
}
}
}
  1. Syntax Errors: Check GLSL version compatibility
  2. Uniform Mismatches: Ensure uniform names match between vertex and fragment
  3. Texture Units: Verify texture unit bindings are correct
  4. Attribute Mismatches: Check vertex attribute formats
  1. Too Many Samples: Optimize blur kernels and reduce sample count
  2. Expensive Operations: Replace expensive math functions with approximations
  3. Memory Bandwidth: Reduce texture lookups and use smaller render targets
  4. State Changes: Minimize shader switches and texture bindings
  1. Precision Issues: Use appropriate precision qualifiers
  2. Gamma Correction: Apply proper gamma correction
  3. Clamping: Ensure values are properly clamped
  4. Alpha Blending: Use correct blend modes for desired effects
assets/modid/
├── shaders/
│ ├── core/
│ │ ├── brightness.vert
│ │ ├── brightness.frag
│ │ ├── gaussian_blur.vert
│ │ ├── gaussian_blur.frag
│ │ └── bloom_combine.frag
│ └── post_processing.json
└── textures/
└── post_processing/
└── (optional textures for effects)

This comprehensive example demonstrates a complete post-processing pipeline with hot reloading, performance optimization, and debugging capabilities, providing a solid foundation for advanced shader integration in Minecraft 26.1 mods.