Shader Integration Example
Shader Integration Example
Section titled “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.
Prerequisites
Section titled “Prerequisites”Before starting this tutorial, you should understand:
- Basic GLSL shader programming
- Minecraft’s render system architecture
- Buffer and texture management
Step 1: Create Post-Processing Framework
Section titled “Step 1: Create Post-Processing Framework”Let’s create a post-processing system similar to Minecraft’s PostChain:
// Example: Custom post-processing managerpublic 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)); }}Step 2: Create Custom Pass Classes
Section titled “Step 2: Create Custom Pass Classes”Base Post-Processing Pass
Section titled “Base Post-Processing Pass”// Example: Base class for post-processing passespublic 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(); }}Bloom Effect Pass
Section titled “Bloom Effect Pass”// Example: Bloom effect implementationpublic 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(); } } }}Step 3: Create Advanced Shader System
Section titled “Step 3: Create Advanced Shader System”Custom Shader Manager
Section titled “Custom Shader Manager”// Example: Advanced shader management systempublic 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(); }}Custom Shader Program Class
Section titled “Custom Shader Program Class”// Example: Custom shader program with advanced featurespublic 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); }}Step 4: Create Shader Files
Section titled “Step 4: Create Shader Files”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);}Gaussian Blur Shader (gaussian_blur.frag)
Section titled “Gaussian Blur Shader (gaussian_blur.frag)”#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 blurconst 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;}Bloom Combine Shader (bloom_combine.frag)
Section titled “Bloom Combine Shader (bloom_combine.frag)”#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 helpervec3 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;}Step 5: Create Hot Reloader
Section titled “Step 5: Create Hot Reloader”// Example: Shader hot reloader for developmentpublic 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 }}Step 6: Integration with Rendering
Section titled “Step 6: Integration with Rendering”Game Integration
Section titled “Game Integration”// Example: Integrate with game renderingpublic 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... }}Testing and Debugging
Section titled “Testing and Debugging”Shader Debugger
Section titled “Shader Debugger”// Example: Shader debugging utilitiespublic 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 }}Performance Optimization
Section titled “Performance Optimization”Shader Caching
Section titled “Shader Caching”// Example: Shader performance optimizationpublic 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(); } }}Common Issues and Solutions
Section titled “Common Issues and Solutions”Shader Compilation Errors
Section titled “Shader Compilation Errors”- Syntax Errors: Check GLSL version compatibility
- Uniform Mismatches: Ensure uniform names match between vertex and fragment
- Texture Units: Verify texture unit bindings are correct
- Attribute Mismatches: Check vertex attribute formats
Performance Issues
Section titled “Performance Issues”- Too Many Samples: Optimize blur kernels and reduce sample count
- Expensive Operations: Replace expensive math functions with approximations
- Memory Bandwidth: Reduce texture lookups and use smaller render targets
- State Changes: Minimize shader switches and texture bindings
Visual Artifacts
Section titled “Visual Artifacts”- Precision Issues: Use appropriate precision qualifiers
- Gamma Correction: Apply proper gamma correction
- Clamping: Ensure values are properly clamped
- Alpha Blending: Use correct blend modes for desired effects
Next Steps
Section titled “Next Steps”- Block Entity Effects Example - Advanced block entity rendering
- Advanced Shader Techniques - Comprehensive shader development
- Performance Optimization - General performance tips
Complete Resource Structure
Section titled “Complete Resource Structure”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.