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

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值