五、渲染管道

在上一篇教程中我们学习了模型类的总体结构,这一篇我们将学习渲染管道,从加载模型开始,一直到实际渲染模型。这里不会深入讨论渲染管道的每个特定部分。我们学习了解一些非常基本的东西。

在上一篇教程中。模型由节点组成,节点自身由节点部分组成。NodePart 是 Model 中最小的部分,它包含关于如何呈现它的所有信息。它包含一个 MeshPart,描述应该呈现什么(形状) ,并包含一个描述应该如何呈现它的 Material。在阅读本教程的这一部分时,请务必牢记这一点。

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.loaders.ModelLoader;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.loader.G3dModelLoader;
import com.badlogic.gdx.graphics.g3d.model.data.ModelData;
import com.badlogic.gdx.graphics.g3d.utils.CameraInputController;
import com.badlogic.gdx.graphics.g3d.utils.TextureProvider;
import com.badlogic.gdx.utils.UBJsonReader;

public class MainGame extends ApplicationAdapter{
	private PerspectiveCamera cam;
	private CameraInputController camCtrol;
	private ModelBatch modelBatch;
	private Environment env;
	
	private ModelData modelData;
	private Model model;
	private ModelInstance modelInstance;
	
	@Override
	public void create() {
		modelBatch = new ModelBatch();
		
		env = new Environment();
		env.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
		env.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));
		
		cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		cam.position.set(0f, 7f, 10f);
		cam.lookAt(0, 0, 0);
		cam.near = 1f;
		cam.far = 300f;
		cam.update();
		
		camCtrol = new CameraInputController(cam);
		Gdx.input.setInputProcessor(camCtrol);
		
		ModelLoader loader =  new G3dModelLoader(new UBJsonReader());
		modelData = loader.loadModelData(Gdx.files.internal("model/scene/invaderscene.g3db"));
		model = new Model(modelData, new TextureProvider.FileTextureProvider());
		modelInstance = new ModelInstance(model);
	}
	
	@Override
	public void resize (int width, int height) {
	}

	@Override
	public void render () {
		camCtrol.update();
		
		Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

		modelBatch.begin(cam);
		modelBatch.render(modelInstance, env);
		modelBatch.end();
	}

	@Override
	public void pause () {
	}

	@Override
	public void resume () {
	}

	@Override
	public void dispose () {
		modelBatch.dispose();
	}
}

前几篇中我们使用 AssetManager 来加载 Model,这在大多数情况下是最佳选择。但有时您可能希望对加载过程有更多的控制。因此本教程不使用 AssetManager 而使用手动来加载模型。

	  ModelLoader loader =  new G3dModelLoader(new UBJsonReader());
		modelData = loader.loadModelData(Gdx.files.internal("model/scene/invaderscene.g3db"));
		model = new Model(modelData, new TextureProvider.FileTextureProvider());

我们创建了一个 ModelLoader,我们在前面使用 libGDX 加载模型时已经看到了它。但是我们现在创建了一个 G3dModelLoader,而不是之前使用的 ObjLoader。我们使用 UBJsonReader 作为参数来构造它,因为 invaderscene.g3db 的文件格式是 json 的二进制文件。对于 g3dj 文件,可以使用 JsonReader。

接下来我们加载 ModelData。ModelData 类包含原始模型数据。它基本上是我们在前面部分看到的文件格式的一对一表示。它不包含任何资源。它只是包含了一个浮点和简短的数组而不是一个网格,对于纹理它只包含文件名而不是实际的纹理。因此,在这个阶段,您可以随心所欲地处理数据,甚至不必考虑 Model 类或任何资源。

最后,在 create ()方法的最后一行,我们使用刚刚加载的 ModelData 构造模型。我们还添加了一个 TextureProvider 作为参数,它将加载纹理作为内部文件。如果您希望对加载纹理进行更多的控制,可以实现 TextureProvider 接口。如果您使用 AssetManager 加载 Model,那么 AssetManager 也用于加载纹理。现在模型和它的资源,比如网格和纹理都被加载了,模型也负责处理它们

接下来我们试一下更改模型的 block1 节点的材质。

		ModelLoader loader =  new G3dModelLoader(new UBJsonReader());
		modelData = loader.loadModelData(Gdx.files.internal("model/scene/invaderscene.g3db"));
		model = new Model(modelData, new TextureProvider.FileTextureProvider());
		
		Material blockMaterial = model.getNode("block1").parts.get(0).material;
		ColorAttribute colorAttribute = (ColorAttribute) blockMaterial.get(ColorAttribute.Diffuse);
		colorAttribute.color.set(Color.YELLOW);
		
		modelInstance = new ModelInstance(model);

我们获取模型的 block1 节点,并得到它的材质。我们在前面的部分中已经看到,这是对“ block_default1 ” 材质的引用,它在所有块节点之间共享。所以改变它,就会改变所有 block 的材质。接下来,我们获取材质的漫反射色彩属性。最后我们将属性的颜色设置为黄色。

image.png

上面的操作需要对模型内部细节足够了解,让我们采取另一种方法。

		ModelLoader loader =  new G3dModelLoader(new UBJsonReader());
		modelData = loader.loadModelData(Gdx.files.internal("model/scene/invaderscene.g3db"));
		model = new Model(modelData, new TextureProvider.FileTextureProvider());
		
		Material blockMaterial = model.getMaterial("block_default1");
		blockMaterial.set(ColorAttribute.createDiffuse(Color.YELLOW));
		
		modelInstance = new ModelInstance(model);

得到的是同样的结果,但是我们现在可以通过 id 直接访问这些材质。并且我们不需要获取当前的漫反射颜色,我们只是设置它。因此,如果材质没有漫反射颜色,它就被添加,否则就会被覆盖。

更改 Model 中的材质将会影响在此之后创建的所有 ModelInstances。你也可以改变每个实例的材质:

		ModelLoader loader =  new G3dModelLoader(new UBJsonReader());
		modelData = loader.loadModelData(Gdx.files.internal("model/scene/invaderscene.g3db"));
		model = new Model(modelData, new TextureProvider.FileTextureProvider());
		
		for (Node block : model.nodes) {
			float r = 0.5f + 0.5f * (float)Math.random();
			float g = 0.5f + 0.5f * (float)Math.random();
			float b = 0.5f + 0.5f * (float)Math.random();
			block.parts.get(0).material.set(ColorAttribute.createDiffuse(r, g, b, 1));
		}
		
		modelInstance = new ModelInstance(model);

与通过节点或它的 ID (这也是可能的)获取材质不同,我们现在只获取第一个材质,因为 ModelInstance 只有一个材质连接到它。

之前我们通过学习 G3DJ 文件了解了 Model 类的结构。现在看一下 ModelInstance 类。

public class ModelInstance implements RenderableProvider {
	public final Array<Material> materials = new Array<Material>();
	public final Array<Node> nodes = new Array<Node>();
	public final Array<Animation> animations = new Array<Animation>();
	public final Model model;
	public Matrix4 transform;
	public Object userData;
	...
}

就像模型一样,它包含一个材质,节点和动画数组。在构建 ModelInstance 时,从模型中复制这些内容,允许您在不影响同一模型的其他 ModelInstances 的情况下修改它们。在创建 ModelInstance 时指定节点 ID 时,只复制影响该节点的材质和动画。因此,就像我们对 block ModelInstance 所做的那样,我们知道第一个材质将总是影响指定的块节点

注意,与 Model 类不同,ModelInstance 没有 Mesh 和 MeshPart 数组。这些不是复制的,而是通过 Node (NodePart)引用的。所以网格仍然在多个模型实例之间共享。对于材质可能包含的任何纹理也是如此。

ModelInstance 类中的模型字段是对它所创建的 Model 的引用。转换字段表示这个特定模型实例的位置、旋转和缩放。我们以前在加载场景教程中见过这种情况。请注意,这个字段不是最终的,这意味着如果需要,您可以为它设置另一个 matrix4引用。最后,userData 字段是一个用户定义的值,可以设置为您可能需要的任何值。例如,您可以使用此值向着色器提供额外的指令(稍后将详细介绍)

注意,ModelInstance 实现了 RenderableProvider 接口。当我们调用 modelBatch.render(instance, lights); 时,ModelBatch 实际上期望一个 RenderableProvider 而不是 ModelInstance。实现可渲染提供程序的类向 ModelBatch 提供一个或多个可渲染对象。看看 Renderable 类:

public class Renderable {
	/** the model transform **/
	public final Matrix4 worldTransform = new Matrix4();
	/** The MeshPart that contains the shape to render **/
	public MeshPart meshPart;
	/** the material to be applied to the mesh **/
	public Material material;
	/** the bones transformations used for skinning, or null if not applicable */  
	public Matrix4 bones[];
	/** the Environment to be used to render this Renderable, may be null **/
	public Environment environment;
	/** the Shader to be used to render this Renderable, may be null **/
	public Shader shader;
	/** user definable value. */
	public Object userData;
}

正如我们之前看到的,NodePart 是描述应该如何渲染的模型中最小的部分。它包含一个 MeshPart 和一个 Material。渲染还包含这些值(展平)以及所提供的转换、灯光、着色器和 userData。因此,当您调用 ModelBatch.render (ModelInstance)时,ModelInstance 中的所有节点部件都将转换为可渲染实例并提供给 ModelBatch。下面代码我们来手动操作一下:

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.loaders.ModelLoader;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.Renderable;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.loader.G3dModelLoader;
import com.badlogic.gdx.graphics.g3d.model.NodePart;
import com.badlogic.gdx.graphics.g3d.model.data.ModelData;
import com.badlogic.gdx.graphics.g3d.utils.CameraInputController;
import com.badlogic.gdx.graphics.g3d.utils.TextureProvider;
import com.badlogic.gdx.utils.UBJsonReader;

public class MainGame extends ApplicationAdapter{
	private PerspectiveCamera cam;
	private CameraInputController camCtrol;
	private ModelBatch modelBatch;
	private Environment env;
	
	private ModelData modelData;
	private Model model;
	private Renderable renderable;
	
	@Override
	public void create() {
		modelBatch = new ModelBatch();
		
		env = new Environment();
		env.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
		env.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));
		
		cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		cam.position.set(0f, 7f, 10f);
		cam.lookAt(0, 0, 0);
		cam.near = 1f;
		cam.far = 300f;
		cam.update();
		
		camCtrol = new CameraInputController(cam);
		Gdx.input.setInputProcessor(camCtrol);
		
		ModelLoader loader =  new G3dModelLoader(new UBJsonReader());
		modelData = loader.loadModelData(Gdx.files.internal("model/scene/invaderscene.g3db"));
		model = new Model(modelData, new TextureProvider.FileTextureProvider());
		
		NodePart blockPart = model.getNode("ship").parts.get(0);
		
		renderable = new Renderable();
		renderable.meshPart.set(blockPart.meshPart);
		renderable.material = blockPart.material;
		renderable.environment = env;
		renderable.worldTransform.idt(); 
	}
	
	@Override
	public void resize (int width, int height) {
	}

	@Override
	public void render () {
		camCtrol.update();
		
		Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

		modelBatch.begin(cam);
		modelBatch.render(renderable);
		modelBatch.end();
	}

	@Override
	public void pause () {
	}

	@Override
	public void resume () {
	}

	@Override
	public void dispose () {
		modelBatch.dispose();
		model.dispose();
	}
}

image.png

在这里,我们像以前一样加载 invaderscene.g3db,但是现在我们获取 ship 节点的第一个 NodePart。从这个 NodePart 中,我们创建一个 Renderable,将每个值设置为 NodePart 的对应值。我们还设置了 Renderable 的灯光,并确保 worldTransform 被设置为位置(0,0,0)的标识意义,而不是旋转和缩放。我们删除了 ModelInstances,而在 render 方法中,我们现在只向 ModelBatch 提供可渲染的内容。我还将相机设置得更接近原点,以获得更好的视角。

ModelInstance 负责提供可渲染的实例,ModelBatch 负责渲染这些可渲染的实例。但是 ModelBatch 实际上并不渲染它们。取而代之的是对它们进行排序,使它们呈现出最优的顺序,然后将它们传递给渲染器的着色器。注意,如果没有提供着色器(或者提供的着色器不合适) ,ModelBatch 将为我们创建着色器。它通过向 ShaderProvider 请求所需的着色器来实现这一点。我们现在不会进一步研究这个问题,但是请注意,在构建 ModelBatch 时,您可以提供自己的 ShaderProvider。

因此,着色器负责渲染一个可渲染的渲染。它做任何它需要做的事情来渲染所提供的渲染。不像名字可能暗示它可以是一个 OpenGL ES 1. x 着色器。对于 OpenGL ES 2.0,它将封装一个 ShaderProgram 并根据指定的 Renderable 设置制服和属性。

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.loaders.ModelLoader;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.Renderable;
import com.badlogic.gdx.graphics.g3d.Shader;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.loader.G3dModelLoader;
import com.badlogic.gdx.graphics.g3d.model.NodePart;
import com.badlogic.gdx.graphics.g3d.model.data.ModelData;
import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader;
import com.badlogic.gdx.graphics.g3d.utils.CameraInputController;
import com.badlogic.gdx.graphics.g3d.utils.DefaultTextureBinder;
import com.badlogic.gdx.graphics.g3d.utils.RenderContext;
import com.badlogic.gdx.graphics.g3d.utils.TextureProvider;
import com.badlogic.gdx.utils.UBJsonReader;

public class MainGame extends ApplicationAdapter{
	private PerspectiveCamera cam;
	private CameraInputController camCtrol;
	private Environment env;
	
	private ModelData modelData;
	private Model model;
	private Renderable renderable;
	private RenderContext renderContext;
	private Shader shader;
	
	@Override
	public void create() {
		
		env = new Environment();
		env.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
		env.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));
		
		cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		cam.position.set(0f, 7f, 10f);
		cam.lookAt(0, 0, 0);
		cam.near = 1f;
		cam.far = 300f;
		cam.update();
		
		camCtrol = new CameraInputController(cam);
		Gdx.input.setInputProcessor(camCtrol);
		
		ModelLoader loader =  new G3dModelLoader(new UBJsonReader());
		modelData = loader.loadModelData(Gdx.files.internal("model/scene/invaderscene.g3db"));
		model = new Model(modelData, new TextureProvider.FileTextureProvider());
		
		NodePart blockPart = model.getNode("ship").parts.get(0);
		
		renderable = new Renderable();
		renderable.meshPart.set(blockPart.meshPart);
		renderable.material = blockPart.material;
		renderable.environment = env;
		renderable.worldTransform.idt(); 
		
		renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.LRU, 1));
		
		shader = new DefaultShader(renderable);
		shader.init();
	}
	
	@Override
	public void resize (int width, int height) {
	}

	@Override
	public void render () {
		camCtrol.update();
		
		Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

		renderContext.begin();
		shader.begin(cam, renderContext);
		shader.render(renderable);
		shader.end();
		renderContext.end();
	}

	@Override
	public void pause () {
	}

	@Override
	public void resume () {
	}

	@Override
	public void dispose () {
		shader.dispose();
		model.dispose();
	}
}

在这里,我们删除了 ModelBatch 并添加了渲染上下文和着色器。渲染上下文保持 OpenGL 状态的跟踪,以消除着色器交换机之间的状态切换。例如,如果一个纹理已经被绑定,它就不需要再次被绑定。我们使用 DefaultTextureBinder 构建渲染上下文,它可以跟踪哪个纹理绑定到哪个纹理单元,并尝试通过重用纹理单元来消除纹理绑定。接下来,我们将着色器构造为 DefaultShader,可渲染的作为参数。请注意,DefaultShader 是一个 OpenGL ES 2.0着色器,因此必须启用 gles20才能工作。

在 render 方法中,我们调用 renderContex.begin ()来确保上下文处于初始状态。接下来我们调用 shader. 开始指示 shader 做任何它需要做的事情来开始渲染。这将设置任何全球制服,如投影矩阵等。然后我们使用着色器渲染渲染。最后我们调用 shader. end 和 renderContext.end 来清理事情。

总结一下:

  • ModelInstance 包含模型的节点和材料的副本。但是它只引用模型的资源,比如网格和纹理。ModelInstance 为其包含的每个 NodePart 创建可渲染的实例。
  • Renderable 是最小的可渲染部分,它通过渲染管道传递。
  • ModelBatch 为每个 Renderable 收集一个着色器,并对渲染对象进行排序,以确保它们以最佳顺序呈现。
  • Shader 负责实际渲染渲染。最常见的应用程序使用多个着色器,其中每个着色器专用于单个着色程序(GLSL 程序)。RenderContext 用于跟踪 OpenGL 上下文,比如当前绑定的纹理。