Libgdx New 3D API 教程之 -- 使用Libgdx创建Shader

来源:互联网 发布:数据库删除一行数据 编辑:程序博客网 时间:2024/05/22 05:19

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方法中设置:

@Overridepublic void begin (Camera camera, RenderContext context) {    this.camera = camera;    this.context = context;    program.begin();    program.setUniformMatrix("u_projTrans", camera.combined);}

u_worldTrans是与renderable对象相关的,所以我们在render方法中设置:

@Overridepublic 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写文章的速度有点慢,这下一章,可能得半个月到一个月。)