六、创建着色器

正如我们之前所学习的,着色器负责渲染的实际渲染。LibGDX 附带的默认着色器支持所需的大部分基本呈现。然而,对于更高级的渲染,比如特效,你可能需要使用自定义着色器。

在我们深入探讨着色器之前,让我们先从一个简单的例子开始。对于这个,我们从上一个教程中的地方开始:

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();
	}
}

我们稍微修改一下代码:

@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, 3f, 5f);
		cam.lookAt(0, 0, 0);
		cam.near = 1f;
		cam.far = 300f;
		cam.update();
		
		camCtrol = new CameraInputController(cam);
		Gdx.input.setInputProcessor(camCtrol);
		
		ModelBuilder modelBuilder = new ModelBuilder();
		model = modelBuilder.createSphere(2f, 2f, 2f, 20, 20
				, new Material(),
		      	 Usage.Position | Usage.Normal | Usage.TextureCoordinates);
		
		NodePart blockPart = model.nodes.get(0).parts.get(0);
        
		renderable = new Renderable();
		blockPart.setRenderable(renderable);
		renderable.environment = null;
		renderable.worldTransform.idt();

		renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.LRU, 1));
		shader = new DefaultShader(renderable);
		shader.init();
}

image.png

在这里,我们删除了 environment 对象,并简单地将 renderable.environment 设置为 null,表示不应该应用任何环境(例如灯光)。接下来,我们删除了 ModelLoader,而是使用我们前面使用过的 ModelBuilder 来创建一个简单的球体。球体的边界是[2,2,2] ,球体有一个空的材质,球体的每个顶点都有位置、法线和纹理坐标属性。

事实上,它看起来几乎就像一个圆而不是一个球。为了确保我们知道我们正在渲染什么,在 create 方法中添加以下行:

renderable.meshPart.primitiveType = GL20.GL_POINTS;

image.png

请注意,您可以通过在屏幕上拖动来旋转相机。这里我们可以看到球面包含的每个顶点。如果你仔细观察,你会发现这个球体是由20个逐渐大小和间隔的圆圈(从下到上)组成的,每个圆圈包含20个点(围绕 y 轴)。它与我们在创建球面时指定的 divisionsU 和 divisionsV 参数匹配。我假设您熟悉顶点和网格,所以我不会深入讨论这个问题。但是请记住顶点(上图中的点)和片段(网格的每个可见像素)之间的区别。

然后删除添加的 renderable.meshPart.primitiveType = GL20.GL_POINTS;

现在让我们通过自定义默认着色器来让这个球体更有趣一点。这是通过两个代表着着色器代码的 glsl 文件来完成的。一个是为球体内的每个顶点执行的(如上所示的点) ,另一个是为球体上的每个像素(片段)执行的。因此,在 assets 文件夹的 data 文件夹中创建两个空文件,并将它们命名为 test.vertex.glsl 和 test.fragment.glsl。

image.png

首先,我们将编写 test.vertex.glsl 文件:

attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec2 a_texCoord0;

uniform mat4 u_worldTrans;
uniform mat4 u_projViewTrans;

varying vec2 v_texCoord0;

void main() {
	v_texCoord0 = a_texCoord0;
	gl_Position = u_projViewTrans * u_worldTrans * vec4(a_position, 1.0);
}

这里我们首先定义三个属性: a_position、 a_normal 和 a_texcoord0。这些将与每个顶点的位置,法线和纹理坐标值一起设置。接下来我们定义两个uniforms。接收 renderable.transform 值,u_projViewTrans 将被设置为 cam.combined 值。请注意,这些变量的命名是在 DefaultShader 类中定义的,稍后将详细介绍。最后,我们定义了一个varying vec2:v_texCoord0,我们将使用它将 a_texCoord0 值传输到片段着色器。

主要的方法是对每个顶点调用的方法。在这里,我们将 a_texCoord0 的值分配给 v_texCoord0,然后计算顶点在屏幕上的位置。

现在我们编写 test.fragment.glsl 文件:

#ifdef GL_ES 
precision mediump float;
#endif

varying vec2 v_texCoord0;

void main() {
	gl_FragColor = vec4(v_texCoord0, 0.0, 1.0);
}

首先,在使用 OpenGL ES 时,我们使用宏来设置精度(例如 Android、 iOS 或 webGL)。接下来我们定义可变的 v_texcoord0,就像我们在顶点着色器中做的那样。

在主方法中,我们将片段颜色的红色分量设置为纹理 x 坐标(u) ,将绿色分量设置为纹理 y 坐标(v)。

现在我们已经有了 glsl 文件,让我们自定义默认着色器来使用它们:

public void create () {
       ...
       String vert = Gdx.files.internal("data/test.vertex.glsl").readString();
       String frag = Gdx.files.internal("data/test.fragment.glsl").readString();
       shader = new DefaultShader(renderable, new DefaultShader.Config(vert, frag));
       shader.init();
}

在这里,我们将刚才创建的两个文件读入一个字符串,并在创建 DefaultShader 时提供这些文件。让我们运行它:

image.png

看起来差不多。球体的红色和绿色分量被设置为 x (u)和 y (v)纹理坐标。因此,只需要几行代码,您就可以使用自己的 GLSL 代码定制 DefaultShader。

但是这只有在你的着色器使用与 DefaultShader 相同的属性和制服时才能起作用。或者换句话说,DefaultShader 提供了一个可以运行自定义 GLSL 代码的 GLSL 上下文。

现在让我们来看看这里到底发生了什么。

我们刚才写的 GLSL 代码在 GPU 上运行。设置顶点属性(a_position,等等)和uuniforms (u_worldtrans,等等) ,为 GPU 提供网格,并可选地为 GPU 提供纹理等,都是在 CPU 完成的。GPU 和 CPU 部分必须一起工作,以呈现可渲染。例如,在 CPU 上绑定一个纹理,而不在 GPU 上使用,这是没有意义的。而且在 GPU 上使用不在 CPU 上设置的统一标准也是没有意义的。在 LibGDX 中,CPU 和 GPU 部件共同构成一个着色器。它可以做任何它需要做的事情(但不应该超过这个范围)来呈现所提供的渲染。

这可能有点令人困惑,因为在大多数文学作品中,着色器指的只是 GPU 部分(GLSL 代码)。在 LibGDX 中,GPU 部分(顶点着色器和片段着色器的组合)称为 ShaderProgram。着色器是 CPU 和 GPU 部分的结合。一般来说,着色器使用着色程序,但是它不一定要使用着色程序(例如 OpenGL ES 1.x)。

现在我们要从头开始编写一个 Shader,而不是依赖于 DefaultShader,所以创建一个名为 TestShader 的新类:

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.g3d.Renderable;
import com.badlogic.gdx.graphics.g3d.Shader;
import com.badlogic.gdx.graphics.g3d.utils.RenderContext;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.utils.GdxRuntimeException;

public class TestShader implements Shader{
	
	@Override
	public void init() {
		
	}
	
	@Override
	public void dispose() {
		
	}

	@Override
	public void begin(Camera camera, RenderContext context) {
		
	}

	@Override
	public void render(Renderable renderable) {
		
	}

	@Override
	public void end() {
		
	}
	
	@Override
	public int compareTo(Shader other) {
		return 0;
	}

	@Override
	public boolean canRender(Renderable instance) {
		return false;
	}
}

在我们实现这个着色器之前,我们首先看看最后两个方法。compareTo 方法被 ModelBatch 用来决定首先使用哪个着色器,我们现在不会使用它。canRender 方法将用于决定着色器应该用于呈现指定的渲染,我们将很快进行学习。但是现在我们总是返回 true。

Init ()方法被调用一次,就在创建着色器之后。这里我们可以创建我们将要使用的 ShaderProgram:

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.g3d.Renderable;
import com.badlogic.gdx.graphics.g3d.Shader;
import com.badlogic.gdx.graphics.g3d.utils.RenderContext;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.utils.GdxRuntimeException;

public class TestShader implements Shader{
	private ShaderProgram program;
	
	@Override
	public void init() {
		String vert = Gdx.files.internal("data/test.vertex.glsl").readString();
		String frag = Gdx.files.internal("data/test.fragment.glsl").readString();
		program = new ShaderProgram(vert, frag);
		if (!program.isCompiled()) {
			throw new GdxRuntimeException(program.getLog());
		}
	}
	
	@Override
	public void dispose() {
		program.dispose();
	}

	@Override
	public void begin(Camera camera, RenderContext context) {
		
	}

	@Override
	public void render(Renderable renderable) {
		
	}

	@Override
	public void end() {
		
	}
	
	@Override
	public int compareTo(Shader other) {
		return 0;
	}

	@Override
	public boolean canRender(Renderable instance) {
		return true;
	}
}

在 init 方法中,我们读取顶点和片段 GLSL 文件,并使用它们构造 ShaderProgram。如果 ShaderProgram 没有成功编译,我们就抛出一个有意义的异常,这样我们就可以轻松地调试 GLSL 代码。Shader 程序需要被处理,因此我们还在我们的 dispose 方法中添加了一行。

当着色器将用于呈现一个或多个渲染对象时,begin 方法将被调用到每个帧。当使用着色器的呈现准备就绪时,end ()方法将在每个帧中调用。Render 方法只会在 begin ()和 end ()调用之间调用。因此,begin ()方法可以用来绑定ShaderProgram, ShaderProgram在新版本中已经不需要解除了:

public class TestShader implements Shader {
	...
	@Override
	public void begin (Camera camera, RenderContext context) {
		program.bind();
	}
	...
	@Override
	public void end () {
		
	}
	...
}

Begin 方法有两个参数,camera 和 context,在调用 end 方法之前,这两个参数对于我们的着色器是独占的(不会改变)。让我们缓存它们:

public class TestShader implements Shader {
	ShaderProgram program;
	Camera cam;
	RenderContext ctx;
	...
	@Override
	public void begin (Camera camera, RenderContext context) {
		cam = camera;
		ctx = context;
		program.bind();
	}
	...
}

着色程序需要两个 uniforms 属性,分别是 u_worldTrans 和 u_projViewTrans。后者只依赖于相机,这意味着我们可以在 begin 方法中设置:

@Override
public void begin(Camera camera, RenderContext context) {
		cam = camera;
		ctx = context;
		
		program.bind();
		program.setUniformMatrix("u_projViewTrans", camera.combined);
}

依赖于可渲染的值,所以我们必须在 render ()方法中设置这个值:

@Override
public void render (Renderable renderable) {
		program.setUniformMatrix("u_worldTrans", renderable.worldTransform);
}

现在我们已经设置了所有的 uniforms 属性,我们需要设置属性,绑定网格并渲染它。这可以通过单个调用 mesh.render ()来实现:

@Override
public void render (Renderable renderable) {
		program.setUniformMatrix("u_worldTrans", renderable.worldTransform);
		renderable.meshPart.render(program);
}

自定义的着色器已经准备就绪,接下来使用它:

public class MainGame extends ApplicationAdapter{
	...
	
	@Override
	public void create() {
		...
		renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.LRU, 1));
		
		shader = new TestShader() ;
		shader.init();
	}
	...
}

image.png

看起来不太对。这是因为我们没有将 RenderContext 设置为使用深度测试。所以让我们改变这一点。同时,我们也可以进行背面剔除。启用背面剔除将不会渲染背面(三角形)不面向相机。因此,如果摄像机在球体内部,你就看不到球体(你可以通过缩放球体来测试) :

@Override
public void begin(Camera camera, RenderContext context) {
		cam = camera;
		ctx = context;
		
		program.bind();
		program.setUniformMatrix("u_projViewTrans", camera.combined);
		ctx.setDepthTest(GL20.GL_LEQUAL);
		ctx.setCullFace(GL20.GL_BACK);
}

image.png

我们看下 render方法中的这一句代码:

program.setUniformMatrix("u_worldTrans", renderable.worldTransform);

在这里,我们将名为 u_worldtrans 的 uniform 设置为 renderable.worldTransform 的值。但是,这意味着每次调用 render ()方法时,ShaderProgram 必须在程序中找到 u_worldtrans 的位置(这是一个开销很大的 String 查找)。这同样适用于 u_projviewtrans。我们可以通过缓存这两个变量来优化:

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g3d.Renderable;
import com.badlogic.gdx.graphics.g3d.Shader;
import com.badlogic.gdx.graphics.g3d.utils.RenderContext;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.utils.GdxRuntimeException;

public class TestShader implements Shader{
	private ShaderProgram program;
	private Camera cam;
	private RenderContext ctx;
	
	private int u_projViewTrans;
	private int u_worldTrans;
	
	@Override
	public void init() {
		String vert = Gdx.files.internal("data/test.vertex.glsl").readString();
		String frag = Gdx.files.internal("data/test.fragment.glsl").readString();
		program = new ShaderProgram(vert, frag);
		
		u_projViewTrans = program.getUniformLocation("u_projViewTrans");
		u_worldTrans = program.getUniformLocation("u_worldTrans");
		
		if (!program.isCompiled()) {
			throw new GdxRuntimeException(program.getLog());
		}
	}
	
	@Override
	public void dispose() {
		program.dispose();
	}

	@Override
	public void begin(Camera camera, RenderContext context) {
		cam = camera;
		ctx = context;
		
		program.bind();
		program.setUniformMatrix(u_projViewTrans, camera.combined);
		ctx.setDepthTest(GL20.GL_LEQUAL);
		ctx.setCullFace(GL20.GL_BACK);
	}

	@Override
	public void render(Renderable renderable) {
		program.setUniformMatrix(u_worldTrans, renderable.worldTransform);
		renderable.meshPart.render(program);
	}

	@Override
	public void end() {
		
	}
	
	@Override
	public int compareTo(Shader other) {
		return 0;
	}

	@Override
	public boolean canRender(Renderable instance) {
		return true;
	}
}

现在,我们使用 LibGDX 3D api 创建了第一个非常基本的着色器。