This blog is a chinese version of xoppa's Libgdx new 3D api tutorial. For English version, please refer to >>LINK<<
这篇教程主要讲怎样利用Libgdx 3D API来创建,并使用Shader相关的基础知识. 我们会看到如何通过DefaultShader使用一段自定义的GLSL。然后我们还会讲到创建一个自定义的shader.
在前面我们已经讲过,Shader是负责渲染Renderable对象的。Libgdx提供的DefaultShader,提供了渲染的最基本需要。然而,对于一些高级的渲染,比如一些特效,你可能就需要自定义shader。
在我们深入之前,先看一下前面教程中写到的例子:
public class ShaderTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public Shader shader;
public RenderContext renderContext;
public Model model;
public Lights lights;
public Renderable renderable;
@Override
public void create () {
lights = new Lights();
lights.ambientLight.set(0.4f, 0.4f, 0.4f, 1f);
lights.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(2f, 2f, 2f);
cam.lookAt(0,0,0);
cam.near = 0.1f;
cam.far = 300f;
cam.update();
camController = new CameraInputController(cam);
Gdx.input.setInputProcessor(camController);
ModelLoader modelLoader = new G3dModelLoader(new JsonReader());
model = modelLoader.loadModel(Gdx.files.internal("data/invaders.g3dj"));
NodePart blockPart = model.getNode("ship").parts.get(0);
renderable = new Renderable();
blockPart.setRenderable(renderable);
renderable.lights = lights;
renderable.worldTransform.idt();
renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1));
shader = new DefaultShader(renderable.material,
renderable.mesh.getVertexAttributes(),
true, false, 1, 0, 0, 0);
shader.init();
}
@Override
public void render () {
camController.update();
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
renderContext.begin();
shader.begin(cam, renderContext);
shader.render(renderable);
shader.end();
renderContext.end();
}
@Override
public void dispose () {
shader.dispose();
model.dispose();
}
@Override public void resume () {}
@Override public void resize (int width, int height) {}
@Override public void pause () {}
@Override public void dispose () {}
}
注意我改了类的名字(ShaderText),还使用了一个简单的方法(setRenderable)来方便我设置renderable值。第一次创建shader时,用一个尽量简单的renderable更好一些。所以,我们来改一点代码:
public void create () {
cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
cam.position.set(2f, 2f, 2f);
cam.lookAt(0,0,0);
cam.near = 0.1f;
cam.far = 300f;
cam.update();
camController = new CameraInputController(cam);
Gdx.input.setInputProcessor(camController);
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.lights = null;
renderable.worldTransform.idt();
renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1));
shader = new DefaultShader(renderable.material,
renderable.mesh.getVertexAttributes(),
false, false, 1, 0, 0, 0);
shader.init();
}
这里,我们移除了灯光的对象,将renderable的灯光设定为空,表示这场景里没有灯光。还有一点,在构建DefaultShader的时候,我瘵灯光的标记也设置成了false。之后,我们移除了ModelLoader,取而代之的是,使用了一个我们一早用过的ModelBuilder,我们要通过它创建一个球体。球体的边界长是[2, 2, 2],我们赋予了这个球空的材质,然后每一个顶点都有位置信息,法线信息,和纹理映射属性。
如果你启用了OpenGL ES 2.0,并且运行这个测试,你将会看到,一个超无聊的球:
事实上,它看起来充其量就是一个圆。为了让大家看清楚我们这里渲染的是球而不是圆,可以做下面这样的设置:
renderable.primitiveType = GL20.GL_POINTS;
再运行一下代码:
你可以通过鼠标的拖动来旋转camera。
现在,我们可以看到这个球体的全部顶点了。如果仔细看,就可以发现,这个球是由20个大小渐进的圆组成(从底部到顶部),而每个圆都包含了20个点(围绕着Y轴)。这些正对应着,我们在创建这个球体时,指定的参数divisionsU和divisionsV。我假设你对vertices和meshes这些概念都熟悉了,所以这里不多深入。但是要记得vertex(就是上图中那些点),和fragment(mesh中每一个可见的像素点).
继续之前,记得把后加的那一句删掉(renderable.primitiveType = GL20.GL_POINTS;)。好吧,又回到之前那个超无聊的圆了。
现在,我们要改一改那个default shader,让我们的球变得生动一点。我们需要两个glsl文件,定义shader代码。第一个,将作用于我们球体中的每一个vertex,另一个作用于球体的每一个像素点(fragment)。在assets文件夹中,创建两个空文件,分另命名为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_projTrans;
varying vec2 v_texCoord0;
void main() {
v_texCoord0 = a_texCoord0;
gl_Position = u_projTrans * u_worldTrans * vec4(a_position, 1.0);
}
我们首先定义了三个acctribtes:a_position, a_normal 和 a_texCoord0. 这些将被设置为每一个顶点的position(位置), normal(法线), 和texture coordinate(纹理坐标)。
接下来,我们定义了两个uniform,u_worldTrans用来接收renderable.transform值,而u_projTrans被设置为cam.combined值。
注意这些命名都定义在了DefaultShader类中,一会就说到。
最后,我们定义了一个varying: v_texCoord0, 用于将a_texCoord0传递给fragment shader.
main方法是对每一个顶点都会被调用的。在这里,我们将a_texCoord0的值赋给了v_texCoord0,然后计算了顶点在屏幕上的位置。接下来,我们来看一下text.fragment.glsl文件:
#ifdef GL_ES
precision mediump float;
#endif
varying vec2 v_texCoord0;
void main() {
gl_FragColor = vec4(v_texCoord0, 0.0, 1.0);
}
首先,在定义了GL_ES的情况下,我们设置了precision。接下来,定义了v_texCoord0,跟之前在vertex shader中一样。
main函数中,我们把每一点的x坐标设置为红色分量,把y坐标值,设置为了绿色变量。(所以得到一个渐变色的球,这里如果不熟悉,可以在练习时把v_texCoord0分开成0.0, 0.0的二维坐标,然后每一个值都改一改,看看结果。其实这就是一个RGBA)。
我们有了glsl文件了,现在让我们利用这两个文件生成自定义的Shader:
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(vert, frag, renderable.material,
renderable.mesh.getVertexAttributes(),
false, false, 1, 0, 0, 0);
shader.init();
}
我们从那两个文件中读取string到变量中,然后生成DefaultShader。运行一下:
看起来不错,每一点的x轴与y轴的坐标,分别表示了红色与绿色的分量。现在看,只用几行代码,你就可以利用DefaultShder创建自己的Shader(着色器)了。
不过,这只适用于,你的shader属性与uniform与default shader一致时才可以。换句话说,DefaultShader提供了GLSL上下文环境,可以运行你自定义的GLSL代码。
现在看看这里面的机制。
我们刚刚写的GLSL代码是运行在GPU上面的。设置vertex attributes(顶点属性, 如位置),uniforms(如u_worldTrans),给GPU提供mesh,或者还有可选项textures什么的,这些都是CPU扔给GPU的。所以GPU和CPU是一起合作来渲染对象的。如在CPU端绑定了纹理,那GPU不渲染出来是不合理的,或者在GPU端使用到一个uniform,那CPU端需要预先设置好。在Libgdx中,CPU和GPU在一起工作,构成了Shader。它会做所有渲染对象所需要做的。
这里可能有些不清楚,因为大多数书籍文章中都说shader只影响到GPU。可是在Libgdx中,GPU负责的部分被称为ShaderProgram,并且一个Shader是GPU和CPU两部分的组合,大多数情况下,Shader都会使用到ShaderProgram,但不是一定的。
现在来自定义一个Shader,取代DefaultShader,所以新建一个TestShader的类,实现Shader接口:
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 true;
}
}
在开始写代码之前,看一下最后两个方法。compareTo方法是ModelBatch调用来判断先使用哪一个shader,我们现在还用不着。然后canRender方法用来决定只渲染特定的renderable对象。这个很快就会说到。现在,我们只给他return true;就好。
init方法会在shader生成时被调用一次。这里可以放置ShaderProgram的生成代码:
public class TestShader implements Shader {
ShaderProgram program;
@Override
public void init () {
String vert = Gdx.files.internal("data/test.vertext.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();
}
...
}
上面的代码中,我们读取了vertex和fragment的GLSL代码文件,并用他们创建了一个ShaderProgram。如果ShaderProgram没有成功创建的话,我们抛出了一个易读的异常,这样方法我们调式GLSL代码。ShaderProgram对象在使用后需要被销毁,所以,在dispose方法中又加了一行。
如果这个shader要用于渲染对象了,那begin方法每个frame都会调用。end方法也会每帧渲染结束后调用。而render方法仅仅会在begin和end方法之前被调用。因此,begin和end方法可以用于处理绑定,和解除绑定我们的ShaderProgram。
public class TestShader implements Shader {
...
@Override
public void begin (Camera camera, RenderContext context) {
program.begin();
}
...
@Override
public void end () {
program.end();
}
...
}
begin方法有两个参数,camera和context,在我们的shader调用end之前,这些都要保留使用,所以,我们需要在类里保存下来:
public class TestShader implements Shader {
ShaderProgram program;
Camera camera;
RenderContext context;
...
@Override
public void begin (Camera camera, RenderContext context) {
this.camera = camera;
this.context = context;
program.begin();
}
...
}
之前的ShaderProgram方法有两个uniforms,u_worldTrans和u_projTrans。后者的值取决于camera,因此,我们要在begin方法中设置:
@Override
public void begin (Camera camera, RenderContext context) {
this.camera = camera;
this.context = context;
program.begin();
program.setUniformMatrix("u_projTrans", camera.combined);
}
u_worldTrans是与renderable对象相关的,所以我们在render方法中设置:
@Override
public void render (Renderable renderable) {
program.setUniformMatrix("u_worldTrans", renderable.worldTransform);
}
现在,uniforms都设置好了,我们不宁设置属性值,与mesh绑定,然后渲染。这些只要调用一个mesh.render()就好:
public class TestShader implements Shader {
...
@Override
public void render (Renderable renderable) {
program.setUniformMatrix("u_worldTrans", renderable.worldTransform);
renderable.mesh.render(program,
renderable.primitiveType,
renderable.meshPartOffset,
renderable.meshPartSize);
}
...
}
新的Shader就好了,我们用一下:
public class ShaderTest extends GdxTest {
...
@Override
public void create () {
...
renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1));
shader = new TestShader();
shader.init();
}
...
}
运行结果:
咦,不对哦。我们还没设置RenderContext,让它使用深度测试,所以要改一下。再有,如果我们这么改了,还要启用backface culling。这个启用后,render就不会去渲染背对着相机的点线面。如果说,我们的相机是在球体里面,那你将看不到任何东西,(可以放大一下试试)。
public class TestShader implements Shader {
...
@Override
public void begin (Camera camera, RenderContext context) {
this.camera = camera;
this.context = context;
program.begin();
program.setUniformMatrix("u_projTrans", camera.combined);
context.setDepthTest(true, GL20.GL_LEQUAL);
context.setCullFace(GL20.GL_BACK);
}
这回看起来像回事了:
完工,现在我们的shader已经可以完成CPU和GPU两部分工作了。但在今天结束之前,我们再看多点东西:
program.setUniformMatrix("u_worldTrans", renderable.worldTransform);
这里,我们将u_worldTrans的值,设成了renderable.worldTransform。这就意味着,ShaderProgram在每次render被调用时都要去寻址字符串“u_worldTrans”。u_projTrans也是这样。所以我们要通过保存他们的址来,来实现优化:
public class TestShader implements Shader {
ShaderProgram program;
Camera camera;
RenderContext context;
int u_projTrans;
int u_worldTrans;
@Override
public void init () {
...
u_projTrans = program.getUniformLocation("u_projTrans");
u_worldTrans = program.getUniformLocation("u_worldTrans");
}
...
@Override
public void begin (Camera camera, RenderContext context) {
this.camera = camera;
this.context = context;
program.begin();
program.setUniformMatrix(u_projTrans, camera.combined);
context.setDepthTest(true, GL20.GL_LEQUAL);
context.setCullFace(GL20.GL_BACK);
}
@Override
public void render (Renderable renderable) {
program.setUniformMatrix(u_worldTrans, renderable.worldTransform);
renderable.mesh.render(program,
renderable.primitiveType,
renderable.meshPartOffset,
renderable.meshPartSize);
}
...
}
现在,我们已经使用libgdx 3d api,创建了最基本的shader。下一篇文件,我们会看一下shader中的材质属性,并且,如何同时使用DefaultShader和你自己创建的Shader.
(泽注:现在xoppa写文章的速度有点慢,这下一章,可能得半个月到一个月。)