ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# Minecraft贴图加载原理 **难度分级:☆☆☆☆☆☆☆** 这一章我们将深入了解Minecraft贴图的加载原理,初步探索OpenGL的纹理系统,并编写一个纹理动态加载器来**从外部读取贴图**。 **警告:在开始阅读之前请务必确保你已经基本掌握了基础篇8和进阶篇1的内容。** ## `bindTexture()`的原理和纹理加载流程    难度分级:☆☆☆☆☆☆ 我们先看一下原版的`bindTexture()`是怎么实现的: ~~~ public void bindTexture(ResourceLocation resource) { ITextureObject itextureobject = this.mapTextureObjects.get(resource); if (itextureobject == null) { itextureobject = new SimpleTexture(resource); this.loadTexture(resource, itextureobject); } TextureUtil.bindTexture(itextureobject.getGlTextureId()); // 这个方法直接调用了GLStateManager的bindTexture()方法 } ~~~ 这是`GLStateManager`的`bindTexture()`方法: ~~~ public static void bindTexture(int texture) { if (texture != textureState[activeTextureUnit].textureName) { textureState[activeTextureUnit].textureName = texture; GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture); } } ~~~ 很明显,它只是获取了一个**TextureID**,**判断当前绑定的是不是这个贴图**,如果不是就调用**OpenGL的底层API**绑定贴图。 那么**TextureID**又是从哪来的呢?怎么**把实际的贴图与一个TextureID相关联**? 让我们看一下`ITextureObject`的一个实现类`SimpleTexture`的源码(已除去不必要的代码): ~~~ @SideOnly(Side.CLIENT) public class SimpleTexture extends AbstractTexture { protected final ResourceLocation textureLocation; public SimpleTexture(ResourceLocation textureResourceLocation) { this.textureLocation = textureResourceLocation; } public void loadTexture(IResourceManager resourceManager) throws IOException { this.deleteGlTexture(); IResource iresource = resourceManager.getResource(this.textureLocation); BufferedImage bufferedimage = TextureUtil.readBufferedImage(iresource.getInputStream()); TextureUtil.uploadTextureImageAllocate(this.getGlTextureId(), bufferedimage, false, false); } } ~~~ 继续往下查看,我们可以发现`glTextureId`是通过调用`GL11.glGenTextures()`获取的。 `TextureUtil.uploadTextureImageAllocate()`方法牵扯到Mipmap等硬核内容,本章不讲述。 此时,我们已经基本搞懂了纹理的加载流程: 1. 从一个`InputStream`里**读取一个`BufferedImage`** 2. **获取一个`TextureId`** 3. **将`BufferedImage`和`TextureId`关联起来,写入到纹理缓冲区里** ## 编写纹理动态加载器    难度分级:☆☆☆☆☆ 在了解了纹理加载流程后,编写一个纹理动态加载器应该没有那么难了,只需要调用Minecraft上层API即可。 我们先编写一个实现了`ITextureObject`的类,因为`AbstractTexture`类已经帮我们接管大部分底层操作,我们直接继承它就行。 ~~~ private static final class ExternalImageTexture extends AbstractTexture { private BufferedImage image; public ExternalImageTexture(File file) throws FileNotFoundException, IOException { this.image = TextureUtil.readBufferedImage(new FileInputStream(file)); } public ExternalImageTexture(BufferedImage image) { this.image = image; } @Override public void loadTexture(IResourceManager resourceManager) throws IOException { this.deleteGlTexture(); TextureUtil.uploadTextureImageAllocate(this.getGlTextureId(), image, false, false); } } ~~~ 以及`bindTexture()`方法: ~~~ public static void bindTexture(File file) throws IOException { ITextureObject texture = new ExternalImageTexture(file); texture.loadTexture(Minecraft.getMinecraft().getResourceManager()); TextureUtil.bindTexture(itextureobject.getGlTextureId()); } public static void bindTexture(BufferedImage image) throws IOException { ITextureObject texture = new ExternalImageTexture(image); texture.loadTexture(Minecraft.getMinecraft().getResourceManager()); TextureUtil.bindTexture(itextureobject.getGlTextureId()); } ~~~ 看起来没有什么问题,但是当我们打开GUI**绘制加载的这些贴图**的时候,我们会感到游戏变得**极度卡顿,游戏内存占用也在飞速上涨**。并且**关掉GUI后内存也没有被回收**。 很明显,我们引发了一个**持续内存泄漏问题**。 我们重新阅读之前写的代码,会发现我们在**每一帧被绘制的时候**都调用了**bindTexture()**方法,这在原版确实没有什么问题,但我们再次看一下原版的`bindTexture()`方法,里面有这么一段代码: ~~~ ITextureObject itextureobject = this.mapTextureObjects.get(resource); if (itextureobject == null) { itextureobject = new SimpleTexture(resource); this.loadTexture(resource, itextureobject); } ~~~ 从这里我们可以看到,原版代码里获取的`ITextureObject`是**已有的,唯一的,可复用的**,而我们刚刚编写的代码在每次`bindTexture()`的时候都**重新构造了一个`ITextureObject`,并重新分配了一个`glTextureId`**,而OpenGL加载的纹理是被默认为**会被再次使用的纹理**,所以它**不会被回收**。 那么我们只需要让我们动态加载的纹理**只会被分配一个**`glTextureId`,并**在GUI被关闭的时候删除贴图**就好了。 我们修改一下刚才的`bindTexture()`方法: ~~~ public static void bindTexture(int textureId) { GlStateManager.bindTexture(textureId); } public static int loadTexture(File file) throws IOException { ITextureObject texture = new ExternalImageTexture(file); texture.loadTexture(Minecraft.getMinecraft().getResourceManager()); return texture.getGlTextureId(); } public static int loadTexture(BufferedImage image) throws IOException { ITextureObject texture = new ExternalImageTexture(image); texture.loadTexture(Minecraft.getMinecraft().getResourceManager()); return texture.getGlTextureId(); } public static void deleteTexture(int textureId) { TextureUtil.deleteTexture(textureId); } ~~~ 我们在GUI被初始化的时候将所有外部纹理加载出来,并获取它的`glTextureId`: ~~~ glTextureId = GLUtils.loadTexture(this.image); // 我把刚才的那些方法写到了GLUtils类中 ~~~ 然后我们绑定纹理的时候仅绑定`textureId`: ~~~ GLUtils.bindTexture(glTextureId); ~~~ 关闭GUI时删除贴图: ~~~ @Override public void onGuiClosed() { GLUtils.deleteTexture(glTextureId); } ~~~ 此时我们打开游戏,开关GUI后,可以发现内存占用不再持续上涨,并且内存也会被及时回收。 本章完。