正如我们之前所学习的,着色器负责渲染的实际渲染。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();
}
在这里,我们删除了 environment 对象,并简单地将 renderable.environment 设置为 null,表示不应该应用任何环境(例如灯光)。接下来,我们删除了 ModelLoader,而是使用我们前面使用过的 ModelBuilder 来创建一个简单的球体。球体的边界是[2,2,2] ,球体有一个空的材质,球体的每个顶点都有位置、法线和纹理坐标属性。
事实上,它看起来几乎就像一个圆而不是一个球。为了确保我们知道我们正在渲染什么,在 create 方法中添加以下行:
renderable.meshPart.primitiveType = GL20.GL_POINTS;
请注意,您可以通过在屏幕上拖动来旋转相机。这里我们可以看到球面包含的每个顶点。如果你仔细观察,你会发现这个球体是由20个逐渐大小和间隔的圆圈(从下到上)组成的,每个圆圈包含20个点(围绕 y 轴)。它与我们在创建球面时指定的 divisionsU 和 divisionsV 参数匹配。我假设您熟悉顶点和网格,所以我不会深入讨论这个问题。但是请记住顶点(上图中的点)和片段(网格的每个可见像素)之间的区别。
然后删除添加的 renderable.meshPart.primitiveType = GL20.GL_POINTS;
现在让我们通过自定义默认着色器来让这个球体更有趣一点。这是通过两个代表着着色器代码的 glsl 文件来完成的。一个是为球体内的每个顶点执行的(如上所示的点) ,另一个是为球体上的每个像素(片段)执行的。因此,在 assets 文件夹的 data 文件夹中创建两个空文件,并将它们命名为 test.vertex.glsl 和 test.fragment.glsl。
首先,我们将编写 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 时提供这些文件。让我们运行它:
看起来差不多。球体的红色和绿色分量被设置为 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();
}
...
}
看起来不太对。这是因为我们没有将 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);
}
我们看下 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 创建了第一个非常基本的着色器。