public class FancyDial { private static final MCLogger logger = MCLogger.getLogger(MCPatcherUtils.CUSTOM_ANIMATIONS, "Animation"); private static final double ANGLE_UNSET = Double.MAX_VALUE; private static final int NUM_SCRATCH_TEXTURES = 3; private static final boolean fboSupported = GLContext.getCapabilities().GL_EXT_framebuffer_object; private static final boolean gl13Supported = GLContext.getCapabilities().OpenGL13; private static final boolean enableCompass = Config.getBoolean(MCPatcherUtils.EXTENDED_HD, "fancyCompass", true); private static final boolean enableClock = Config.getBoolean(MCPatcherUtils.EXTENDED_HD, "fancyClock", true); private static final boolean useGL13 = gl13Supported && Config.getBoolean(MCPatcherUtils.EXTENDED_HD, "useGL13", true); private static final boolean useScratchTexture = Config.getBoolean(MCPatcherUtils.EXTENDED_HD, "useScratchTexture", true); private static final int glAttributes; private static boolean initialized; private static boolean active; private static final int drawList = GL11.glGenLists(1); private static final Map<TextureAtlasSprite, ResourceLocation> setupInfo = new IdentityHashMap<TextureAtlasSprite, ResourceLocation>(); private static final Map<TextureAtlasSprite, FancyDial> instances = new IdentityHashMap<TextureAtlasSprite, FancyDial>(); private static int warnCount; private final TextureAtlasSprite icon; private final PropertiesFile properties; private final String name; private final int x0; private final int y0; private final int width; private final int height; private final ByteBuffer scratchBuffer; private final FBO[] scratchFBO = new FBO[NUM_SCRATCH_TEXTURES]; private FBO itemsFBO; private int scratchIndex; private Map<Double, ByteBuffer> itemFrames = new TreeMap<Double, ByteBuffer>(); private int outputFrames; private boolean ok; private double lastAngle = ANGLE_UNSET; private boolean lastItemFrameRenderer; private final List<Layer> layers = new ArrayList<Layer>(); private InputHandler keyboard; private static final float STEP = 0.01f; private float scaleXDelta; private float scaleYDelta; private float offsetXDelta; private float offsetYDelta; static { logger.config("fbo: supported=%s", fboSupported); logger.config("GL13: supported=%s, enabled=%s", gl13Supported, useGL13); int bits = GL11.GL_VIEWPORT_BIT | GL11.GL_SCISSOR_BIT | GL11.GL_DEPTH_BITS | GL11.GL_LIGHTING_BIT; if (useGL13) { bits |= GL13.GL_MULTISAMPLE_BIT; } glAttributes = bits; GL11.glNewList(drawList, GL11.GL_COMPILE); drawBox(); GL11.glEndList(); } public static void setup(TextureAtlasSprite icon) { if (!fboSupported) { return; } String name = IconAPI.getIconName(icon).replaceFirst("^minecraft:items/", ""); if (icon instanceof TextureClock && name.equals("compass")) { // 1.5 bug name = "clock"; } if ("compass".equals(name)) { if (!enableCompass) { return; } } else if ("clock".equals(name)) { if (!enableClock) { return; } } else { logger.warning("ignoring custom animation for %s not compass or clock", name); return; } ResourceLocation resource = TexturePackAPI.newMCPatcherResourceLocation( "/misc/" + name + ".properties", "dial/" + name + ".properties"); if (TexturePackAPI.hasResource(resource)) { logger.fine("found custom %s (%s)", name, resource); setupInfo.put(icon, resource); active = true; } } public static boolean update(TextureAtlasSprite icon, boolean itemFrameRenderer) { if (!initialized) { logger.finer("deferring %s update until initialization finishes", IconAPI.getIconName(icon)); return false; } if (!active) { return false; } int oldFB = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT); if (oldFB != 0 && warnCount < 10) { logger.finer( "rendering %s while non-default framebuffer %d is active", IconAPI.getIconName(icon), oldFB); warnCount++; } int oldTexture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); try { FancyDial instance = getInstance(icon); return instance != null && instance.render(itemFrameRenderer); } finally { EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFB); GLAPI.glBindTexture(oldTexture); } } static void clearAll() { logger.finer("FancyDial.clearAll"); if (initialized) { active = false; setupInfo.clear(); } for (FancyDial instance : instances.values()) { if (instance != null) { instance.finish(); } } instances.clear(); initialized = true; } static void registerAnimations() { TextureObject texture = TexturePackAPI.getTextureObject(TexturePackAPI.ITEMS_PNG); if (texture instanceof TextureAtlas) { List<TextureAtlasSprite> animations = ((TextureAtlas) texture).animations; for (FancyDial instance : instances.values()) { instance.registerAnimation(animations); } } } void registerAnimation(List<TextureAtlasSprite> animations) { if (animations.contains(icon)) { return; } animations.add(icon); if (icon.animationFrames == null) { icon.animationFrames = new ArrayList<int[]>(); } if (icon.animationFrames.isEmpty()) { int[] dummyRGB = new int[width * height]; Arrays.fill(dummyRGB, 0xffff00ff); icon.animationFrames.add(dummyRGB); } logger.fine("registered %s animation", name); } private static FancyDial getInstance(TextureAtlasSprite icon) { if (instances.containsKey(icon)) { return instances.get(icon); } ResourceLocation resource = setupInfo.remove(icon); instances.put(icon, null); if (resource == null) { return null; } PropertiesFile properties = PropertiesFile.get(logger, resource); if (properties == null) { return null; } try { FancyDial instance = new FancyDial(icon, properties); if (instance.ok) { instances.put(icon, instance); return instance; } instance.finish(); } catch (Throwable e) { e.printStackTrace(); } return null; } private FancyDial(TextureAtlasSprite icon, PropertiesFile properties) { this.icon = icon; this.properties = properties; name = IconAPI.getIconName(icon); x0 = IconAPI.getIconX0(icon); y0 = IconAPI.getIconY0(icon); width = IconAPI.getIconWidth(icon); height = IconAPI.getIconHeight(icon); scratchBuffer = ByteBuffer.allocateDirect(4 * width * height); int itemsTexture = TexturePackAPI.getTextureIfLoaded(TexturePackAPI.ITEMS_PNG); if (itemsTexture < 0) { logger.severe("could not get items texture"); return; } itemsFBO = new FBO(itemsTexture, x0, y0, width, height); if (useScratchTexture) { logger.fine("rendering %s to %dx%d scratch texture", name, width, height); for (int i = 0; i < scratchFBO.length; i++) { scratchFBO[i] = new FBO(width, height); } } else { logger.fine("rendering %s directly to atlas", name); } boolean debug = false; for (int i = 0; ; i++) { Layer layer = newLayer(properties, "." + i); if (layer == null) { if (i > 0) { break; } continue; } layers.add(layer); debug |= layer.debug; logger.fine(" new %s", layer); } keyboard = new InputHandler(name, debug); if (layers.size() < 2) { logger.error("custom %s needs at least two layers defined", name); return; } outputFrames = properties.getInt("outputFrames", 0); int glError = GL11.glGetError(); if (glError != 0) { logger.severe("%s during %s setup", GLU.gluErrorString(glError), name); return; } ok = true; } private boolean render(boolean itemFrameRenderer) { if (!ok) { return false; } if (!itemFrameRenderer) { boolean changed = true; if (!keyboard.isEnabled()) { changed = false; } else if (keyboard.isKeyPressed(Keyboard.KEY_NUMPAD2)) { scaleYDelta -= STEP; } else if (keyboard.isKeyPressed(Keyboard.KEY_NUMPAD8)) { scaleYDelta += STEP; } else if (keyboard.isKeyPressed(Keyboard.KEY_NUMPAD4)) { scaleXDelta -= STEP; } else if (keyboard.isKeyPressed(Keyboard.KEY_NUMPAD6)) { scaleXDelta += STEP; } else if (keyboard.isKeyPressed(Keyboard.KEY_DOWN)) { offsetYDelta += STEP; } else if (keyboard.isKeyPressed(Keyboard.KEY_UP)) { offsetYDelta -= STEP; } else if (keyboard.isKeyPressed(Keyboard.KEY_LEFT)) { offsetXDelta -= STEP; } else if (keyboard.isKeyPressed(Keyboard.KEY_RIGHT)) { offsetXDelta += STEP; } else if (keyboard.isKeyPressed(Keyboard.KEY_MULTIPLY)) { scaleXDelta = scaleYDelta = offsetXDelta = offsetYDelta = 0.0f; } else { changed = false; } if (changed) { logger.info(""); logger.info("scaleX %+f", scaleXDelta); logger.info("scaleY %+f", scaleYDelta); logger.info("offsetX %+f", offsetXDelta); logger.info("offsetY %+f", offsetYDelta); lastAngle = ANGLE_UNSET; } if (outputFrames > 0) { writeCustomImage(); outputFrames = 0; } } double angle = getAngle(icon); if (!useScratchTexture) { // render directly to items.png if (angle != lastAngle) { renderToItems(angle); lastAngle = angle; } } else if (itemFrameRenderer) { // look in itemFrames cache first ByteBuffer buffer = itemFrames.get(angle); if (buffer == null) { logger.fine("rendering %s at angle %f for item frame", name, angle); buffer = ByteBuffer.allocateDirect(width * height * 4); renderToItems(angle); itemsFBO.read(buffer); itemFrames.put(angle, buffer); } else { itemsFBO.write(buffer); } lastItemFrameRenderer = true; } else if (lastAngle == ANGLE_UNSET) { // first time rendering - render all N copies for (FBO fbo : scratchFBO) { renderToFB(angle, fbo); } scratchFBO[0].read(scratchBuffer); itemsFBO.write(scratchBuffer); lastAngle = angle; scratchIndex = 0; } else if (lastItemFrameRenderer || angle != lastAngle) { // render to buffer i + 1 // update items.png from buffer i int nextIndex = (scratchIndex + 1) % NUM_SCRATCH_TEXTURES; if (angle != lastAngle) { renderToFB(angle, scratchFBO[nextIndex]); scratchFBO[scratchIndex].read(scratchBuffer); } itemsFBO.write(scratchBuffer); lastAngle = angle; scratchIndex = nextIndex; lastItemFrameRenderer = false; } int glError = GL11.glGetError(); if (glError != 0) { logger.severe("%s during %s update", GLU.gluErrorString(glError), name); ok = false; } return ok; } private void writeCustomImage() { try { BufferedImage image = new BufferedImage(width, outputFrames * height, BufferedImage.TYPE_INT_ARGB); IntBuffer intBuffer = scratchBuffer.asIntBuffer(); int[] argb = new int[width * height]; File path = MCPatcherUtils.getGamePath("custom_" + name + ".png"); logger.info("generating %d %s frames", outputFrames, name); for (int i = 0; i < outputFrames; i++) { renderToItems(i * (360.0 / outputFrames)); itemsFBO.read(scratchBuffer); intBuffer.position(0); for (int j = 0; j < argb.length; j++) { switch (MipmapHelper.TEX_FORMAT) { case GL12.GL_BGRA: int bgra = intBuffer.get(j); argb[j] = (bgra << 24) | ((bgra & 0xff00) << 8) | ((bgra & 0xff0000) >> 8) | (bgra >>> 24); break; default: if (i == 0 && j == 0) { logger.warning( "unhandled texture format %d, color channels may be incorrect", MipmapHelper.TEX_FORMAT); } // fall through case GL11.GL_RGBA: argb[j] = Integer.rotateRight(intBuffer.get(j), 8); break; } } image.setRGB(0, i * height, width, height, argb, 0, width); } ImageIO.write(image, "png", path); logger.info("wrote %dx%d %s", image.getWidth(), image.getHeight(), path.getPath()); } catch (Throwable e) { e.printStackTrace(); } } private void renderToItems(double angle) { renderToFB(angle, itemsFBO); } private void renderToFB(double angle, FBO fbo) { if (fbo != null) { fbo.bind(); renderImpl(angle); fbo.unbind(); } } private void renderImpl(double angle) { for (Layer layer : layers) { layer.blendMethod.applyBlending(); GL11.glPushMatrix(); TexturePackAPI.bindTexture(layer.textureName); float offsetX = layer.offsetX; float offsetY = layer.offsetY; float scaleX = layer.scaleX; float scaleY = layer.scaleY; if (layer.debug) { offsetX += offsetXDelta; offsetY += offsetYDelta; scaleX += scaleXDelta; scaleY += scaleYDelta; } GL11.glTranslatef(offsetX, offsetY, 0.0f); GL11.glScalef(scaleX, scaleY, 1.0f); float layerAngle = (float) (angle * layer.rotationMultiplier + layer.rotationOffset); GL11.glRotatef(layerAngle, 0.0f, 0.0f, 1.0f); GL11.glCallList(drawList); GL11.glPopMatrix(); } } private static void drawBox() { GL11.glBegin(GL11.GL_QUADS); GL11.glTexCoord2f(0.0f, 0.0f); GL11.glVertex3f(-1.0f, -1.0f, 0.0f); GL11.glTexCoord2f(1.0f, 0.0f); GL11.glVertex3f(1.0f, -1.0f, 0.0f); GL11.glTexCoord2f(1.0f, 1.0f); GL11.glVertex3f(1.0f, 1.0f, 0.0f); GL11.glTexCoord2f(0.0f, 1.0f); GL11.glVertex3f(-1.0f, 1.0f, 0.0f); GL11.glEnd(); } private void finish() { for (int i = 0; i < scratchFBO.length; i++) { if (scratchFBO[i] != null) { scratchFBO[i].delete(); scratchFBO[i] = null; } } if (itemsFBO != null) { itemsFBO.delete(); itemsFBO = null; } itemFrames.clear(); layers.clear(); ok = false; } @Override public String toString() { return String.format("FancyDial{%s, %dx%d @ %d,%d}", name, width, height, x0, y0); } @Override protected void finalize() throws Throwable { finish(); super.finalize(); } private static double getAngle(Icon icon) { if (icon instanceof TextureCompass) { return ((TextureCompass) icon).currentAngle * 180.0 / Math.PI; } else if (icon instanceof TextureClock) { return ((TextureClock) icon).currentAngle * 360.0; } else { return 0.0; } } Layer newLayer(PropertiesFile properties, String suffix) { ResourceLocation textureResource = properties.getResourceLocation("source" + suffix, ""); if (textureResource == null) { return null; } if (!TexturePackAPI.hasResource(textureResource)) { properties.error("could not read %s", textureResource); return null; } float scaleX = properties.getFloat("scaleX" + suffix, 1.0f); float scaleY = properties.getFloat("scaleY" + suffix, 1.0f); float offsetX = properties.getFloat("offsetX" + suffix, 0.0f); float offsetY = properties.getFloat("offsetY" + suffix, 0.0f); float angleMultiplier = properties.getFloat("rotationSpeed" + suffix, 0.0f); float angleOffset = properties.getFloat("rotationOffset" + suffix, 0.0f); String blend = properties.getString("blend" + suffix, "alpha"); BlendMethod blendMethod = BlendMethod.parse(blend); if (blendMethod == null) { properties.error("unknown blend method %s", blend); return null; } boolean debug = properties.getBoolean("debug" + suffix, false); return new Layer( textureResource, scaleX, scaleY, offsetX, offsetY, angleMultiplier, angleOffset, blendMethod, debug); } private class Layer { final ResourceLocation textureName; final float scaleX; final float scaleY; final float offsetX; final float offsetY; final float rotationMultiplier; final float rotationOffset; final BlendMethod blendMethod; final boolean debug; Layer( ResourceLocation textureName, float scaleX, float scaleY, float offsetX, float offsetY, float rotationMultiplier, float rotationOffset, BlendMethod blendMethod, boolean debug) { this.textureName = textureName; this.scaleX = scaleX; this.scaleY = scaleY; this.offsetX = offsetX; this.offsetY = offsetY; this.rotationMultiplier = rotationMultiplier; this.rotationOffset = rotationOffset; this.blendMethod = blendMethod; this.debug = debug; } @Override public String toString() { return String.format( "Layer{%s %f %f %+f %+f x%f}", textureName, scaleX, scaleY, offsetX, offsetY, rotationMultiplier); } } private static class FBO { private final int texture; private final boolean ownTexture; private final int x0; private final int y0; private final int width; private final int height; private final int frameBuffer; private boolean lightmapEnabled; private boolean deleted; FBO(int width, int height) { this(blankTexture(width, height), true, 0, 0, width, height); } FBO(int texture, int x0, int y0, int width, int height) { this(texture, false, x0, y0, width, height); } private FBO(int texture, boolean ownTexture, int x0, int y0, int width, int height) { this.texture = texture; this.ownTexture = ownTexture; this.x0 = x0; this.y0 = y0; this.width = width; this.height = height; frameBuffer = EXTFramebufferObject.glGenFramebuffersEXT(); if (frameBuffer < 0) { throw new RuntimeException("could not get framebuffer object"); } GLAPI.glBindTexture(texture); EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, frameBuffer); EXTFramebufferObject.glFramebufferTexture2DEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, texture, 0); } void bind() { EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, frameBuffer); GL11.glPushAttrib(glAttributes); GL11.glViewport(x0, y0, width, height); GL11.glEnable(GL11.GL_SCISSOR_TEST); GL11.glScissor(x0, y0, width, height); lightmapEnabled = false; if (gl13Supported) { GL13.glActiveTexture(GL13.GL_TEXTURE1); lightmapEnabled = GL11.glIsEnabled(GL11.GL_TEXTURE_2D); if (lightmapEnabled) { GL11.glDisable(GL11.GL_TEXTURE_2D); } GL13.glActiveTexture(GL13.GL_TEXTURE0); } GL11.glEnable(GL11.GL_TEXTURE_2D); GL11.glDisable(GL11.GL_DEPTH_TEST); GLAPI.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); GL11.glDisable(GL11.GL_LIGHTING); GL11.glEnable(GL11.GL_ALPHA_TEST); GLAPI.glAlphaFunc(GL11.GL_GREATER, 0.01f); if (useGL13) { GL11.glDisable(GL13.GL_MULTISAMPLE); } GLAPI.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); GL11.glClear(GL11.GL_COLOR_BUFFER_BIT); GL11.glMatrixMode(GL11.GL_PROJECTION); GL11.glPushMatrix(); GL11.glLoadIdentity(); GL11.glOrtho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f); GL11.glMatrixMode(GL11.GL_MODELVIEW); GL11.glPushMatrix(); GL11.glLoadIdentity(); } void unbind() { GL11.glPopAttrib(); GL11.glMatrixMode(GL11.GL_PROJECTION); GL11.glPopMatrix(); GL11.glMatrixMode(GL11.GL_MODELVIEW); GL11.glPopMatrix(); if (lightmapEnabled) { GL13.glActiveTexture(GL13.GL_TEXTURE1); GL11.glEnable(GL11.GL_TEXTURE_2D); GL13.glActiveTexture(GL13.GL_TEXTURE0); } GL11.glEnable(GL11.GL_BLEND); GLAPI.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0); } void read(ByteBuffer buffer) { EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, frameBuffer); buffer.position(0); GL11.glReadPixels( x0, y0, width, height, MipmapHelper.TEX_FORMAT, MipmapHelper.TEX_DATA_TYPE, buffer); } void write(ByteBuffer buffer) { GLAPI.glBindTexture(texture); buffer.position(0); GL11.glTexSubImage2D( GL11.GL_TEXTURE_2D, 0, x0, y0, width, height, MipmapHelper.TEX_FORMAT, MipmapHelper.TEX_DATA_TYPE, buffer); } void delete() { if (!deleted) { deleted = true; if (ownTexture) { GL11.glDeleteTextures(texture); } EXTFramebufferObject.glDeleteFramebuffersEXT(frameBuffer); } } @Override protected void finalize() throws Throwable { delete(); super.finalize(); } private static int blankTexture(int width, int height) { int texture = GL11.glGenTextures(); MipmapHelper.setupTexture(texture, width, height, "scratch"); return texture; } } }
public GL11BatchVertexRenderer(int mode) { super(mode); displayList = GL11.glGenLists(1); }