天天看點

Opengl ES系列學習--點亮世界

     本節我們在上一節的基礎上繼續添加光照,我們要分析的目标就是《OpenGL ES應用開發實踐指南 Android卷》書中第13章實作的最終的結果,代碼下載下傳請點選:Opengl ES Source Code,該Git庫中的lighting Module就是我們本節要分析的目标,先看下本節最終實作的結果。

Opengl ES系列學習--點亮世界

     在上一節的基礎上,把天空盒換成了一個夜晚的天空盒,同時增加了光照,三個粒子發射器也增加了點光源,可以看到他們發射點的顔色非常亮。在開始分析代碼之前,涉及到光照的幾個知識點我們需要先搞清楚,否則後邊分析代碼時會非常困惑。

     1、用朗伯體反射實作方向光

     要實作漫反射,我們可以使用朗伯體反射。朗伯體反射描述了這樣一個表面,它會反射所有方向上打到它的光線,以使它在任何觀察點上看起來都是一樣的,它的外觀隻依賴于它與光源所處的方位及其距離。我們在代碼中要實作朗伯體反射,需要先計算出表面和光線之間夾角的餘弦值,而要計算餘弦值隻需要計算指向光源的向量與表面法線的點積,它的工作原理是,當兩個向量都是歸一化的向量時,那兩個向量的點積就是它們之間夾角的餘弦值,這也正是朗伯體反射所需要的。來看一段代碼實作,在目前lighting Module當中,它的高度圖頂點着色器heightmap_vertex_shader.glsl中,定義了getDirectionalLighting方法,源碼如下:

vec3 getDirectionalLighting()
{   
    return materialColor * 0.3 
         * max(dot(eyeSpaceNormal, u_VectorToLight), 0.0);       
}
           

     materialColor表示材質光,eyeSpaceNormal表示法線向量,u_VectorToLight表示方向光源,内建函數dot的作用是計算兩個向量的點積,點積的結果也就是它們的餘弦值,再和materialColor相乘得到的結果就是目前平面的朗伯體反射,乘以0.3隻是為了調低光強度,結合這段代碼,大家應該就可以明白我們如何使用朗伯體反射了。

     2、了解點光和方向光

     這點在我們Opengl ES系列學習--序小節的第五個問題點已經描述了,如果還有疑問,請回頭搞清楚。

     3、使用點光源

     1)我們将位置和法線放入眼空間,在這個空間裡,所有的位置和法線都是相對于照相機的位置和方位,這樣做是為了使我們可以在同一個坐标空間中比較所有事物的距離和方位。我們為什麼使用眼空間而不是世界空間呢?因為鏡面光也依賴于照相機的位置,即使我們在本章中沒有使用鏡面光,學習如何使用眼空間也是一個好主意,這樣我們在以後就可以直接使用它了。

     2)要把一個位置放進眼空間中,我們隻需要讓它與模型矩陣相乘,把它放入世界空間中;我們再把它與視圖矩陣相乘,這樣就把它入入眼空間中了。為了簡化操作,我們可以把視力矩陣與模型矩陣相乘得到一個單一的矩陣,稱為模型視圖矩陣,再用這個矩陣把我們的位置放入眼空間中。

     3)如果模型視圖矩陣隻包含平穩和旋轉,這對法線也是有用的,但是,如果我們縮放了一個物體,那會怎麼樣?如果縮放在所有方向上都是一樣的,我們隻需要重新歸一化法線,使它的長度保持為1,但是如果某個方向上被壓扁了,那我們必須要補償那一方向。要達到這個目的,我們可以把倒置模型視圖矩陣,然後再轉置這個倒置的矩陣,讓法線與該矩陣相乘,最後歸一化結果。這塊涉及到很多矩陣運算等高等數學的知識,自己也沒搞清楚,暫時就知道這樣使用就行了。

     第三點使用點光源的文字全部是從書上原本的抄過來的,從這裡大家可以感覺到,要熟練或者精通使用Opengl ES,不光要懂得這些最基本的API,還必須懂得一些高等數學、實體學上的東西,我們才能明白,要實作一個結果,到底要用什麼樣的原理來實作,否則想不通這些,我們根本不知道代碼該怎麼寫,我自己對這些原理性的東西也是一頭霧水。

     講了幾個概念,接下來我們來看代碼實作,還是和以前一樣,關注點放在差異的部分,本節的目錄結構如下:

Opengl ES系列學習--點亮世界

     看一下本節的頂點着色器,代碼比之前多了很多,後續我們要自己實作一些複雜的功能,都要往這個方向發展。控制更多的邏輯肯定是需要更多的代碼了。我們逐個分析每個包下所有檔案的不同。

     data包下完全相同,跳過;objects包下的Heightmap類和之前有差異,我們再次來分析一下,修改後的源碼如下:

public class Heightmap {
    private static final int POSITION_COMPONENT_COUNT = 3;
    private static final int NORMAL_COMPONENT_COUNT = 3;
    private static final int TOTAL_COMPONENT_COUNT =
            POSITION_COMPONENT_COUNT + NORMAL_COMPONENT_COUNT;
    private static final int STRIDE =
            (POSITION_COMPONENT_COUNT + NORMAL_COMPONENT_COUNT) * BYTES_PER_FLOAT;

    private final int width;
    private final int height;
    private final int numElements;

    private final VertexBuffer vertexBuffer;
    private final IndexBuffer indexBuffer;

    public Heightmap(Bitmap bitmap) {
        width = bitmap.getWidth();
        height = bitmap.getHeight();
        if (width * height > 65536) {
            throw new RuntimeException("Heightmap is too large for the index buffer.");
        }
        numElements = calculateNumElements();
        vertexBuffer = new VertexBuffer(loadBitmapData(bitmap));
        indexBuffer = new IndexBuffer(createIndexData());
    }

    /**
     * Copy the heightmap data into a vertex buffer object.
     */
    private float[] loadBitmapData(Bitmap bitmap) {
        final int[] pixels = new int[width * height];
        bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
        bitmap.recycle();

        final float[] heightmapVertices =
                new float[width * height * TOTAL_COMPONENT_COUNT];

        int offset = 0;

        for (int row = 0; row < height; row++) {
            for (int col = 0; col < width; col++) {
                // The heightmap will lie flat on the XZ plane and centered
                // around (0, 0), with the bitmap width mapped to X and the
                // bitmap height mapped to Z, and Y representing the height. We
                // assume the heightmap is grayscale, and use the value of the
                // red color to determine the height.
                final Point point = getPoint(pixels, row, col);

                heightmapVertices[offset++] = point.x;
                heightmapVertices[offset++] = point.y;
                heightmapVertices[offset++] = point.z;

                final Point top = getPoint(pixels, row - 1, col);
                final Point left = getPoint(pixels, row, col - 1);
                final Point right = getPoint(pixels, row, col + 1);
                final Point bottom = getPoint(pixels, row + 1, col);

                final Vector rightToLeft = Geometry.vectorBetween(right, left);
                final Vector topToBottom = Geometry.vectorBetween(top, bottom);
                final Vector normal = rightToLeft.crossProduct(topToBottom).normalize();

                heightmapVertices[offset++] = normal.x;
                heightmapVertices[offset++] = normal.y;
                heightmapVertices[offset++] = normal.z;
            }
        }

        return heightmapVertices;
    }

    /**
     * Returns a point at the expected position given by row and col, but if the
     * position is out of bounds, then it clamps the position and uses the
     * clamped position to read the height. For example, calling with row = -1
     * and col = 5 will set the position as if the point really was at -1 and 5,
     * but the height will be set to the heightmap height at (0, 5), since (-1,
     * 5) is out of bounds. This is useful when we're generating normals, and we
     * need to read the heights of neighbouring points.
     */
    private Point getPoint(int[] pixels, int row, int col) {
        float x = ((float) col / (float) (width - 1)) - 0.5f;
        float z = ((float) row / (float) (height - 1)) - 0.5f;

        row = clamp(row, 0, width - 1);
        col = clamp(col, 0, height - 1);

        float y = (float) Color.red(pixels[(row * height) + col]) / (float) 255;

        return new Point(x, y, z);
    }

    private int clamp(int val, int min, int max) {
        return Math.max(min, Math.min(max, val));
    }

    private int calculateNumElements() {
        // There should be 2 triangles for every group of 4 vertices, so a
        // heightmap of, say, 10x10 pixels would have 9x9 groups, with 2
        // triangles per group and 3 vertices per triangle for a total of (9 x 9
        // x 2 x 3) indices.
        return (width - 1) * (height - 1) * 2 * 3;
    }

    /**
     * Create an index buffer object for the vertices to wrap them together into
     * triangles, creating indices based on the width and height of the
     * heightmap.
     */
    private short[] createIndexData() {
        final short[] indexData = new short[numElements];
        int offset = 0;

        for (int row = 0; row < height - 1; row++) {
            for (int col = 0; col < width - 1; col++) {
                // Note: The (short) cast will end up underflowing the number
                // into the negative range if it doesn't fit, which gives us the
                // right unsigned number for OpenGL due to two's complement.
                // This will work so long as the heightmap contains 65536 pixels
                // or less.
                short topLeftIndexNum = (short) (row * width + col);
                short topRightIndexNum = (short) (row * width + col + 1);
                short bottomLeftIndexNum = (short) ((row + 1) * width + col);
                short bottomRightIndexNum = (short) ((row + 1) * width + col + 1);

                // Write out two triangles.
                indexData[offset++] = topLeftIndexNum;
                indexData[offset++] = bottomLeftIndexNum;
                indexData[offset++] = topRightIndexNum;

                indexData[offset++] = topRightIndexNum;
                indexData[offset++] = bottomLeftIndexNum;
                indexData[offset++] = bottomRightIndexNum;
            }
        }

        return indexData;
    }

    public void bindData(HeightmapShaderProgram heightmapProgram) {
        vertexBuffer.setVertexAttribPointer(0,
                heightmapProgram.getPositionAttributeLocation(),
                POSITION_COMPONENT_COUNT, STRIDE);

        vertexBuffer.setVertexAttribPointer(
                POSITION_COMPONENT_COUNT * BYTES_PER_FLOAT,
                heightmapProgram.getNormalAttributeLocation(),
                NORMAL_COMPONENT_COUNT, STRIDE);
    }

    public void draw() {
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer.getBufferId());
        glDrawElements(GL_TRIANGLES, numElements, GL_UNSIGNED_SHORT, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }
}
           

     大家可以看到,代碼中有非常大段的注釋,這是一個非常好的習慣,在我之前的部落格中,也一直在強調注釋的重要性,我們可以随便翻開android源碼中哪個檔案,都可以看到非常詳細的注釋,注釋能非常好的提高代碼的可讀性,是以我們自己一定要養成這樣的好習慣!不僅要寫注釋,而且寫注釋的時候,每個字、每個詞都要斟酌,力求表達清楚。

     好,我們現在來看一下修改後和之前不同的地方。新增了NORMAL_COMPONENT_COUNT、TOTAL_COMPONENT_COUNT、STRIDE三個成員變量,NORMAL_COMPONENT_COUNT表示描述法線需要3個size;TOTAL_COMPONENT_COUNT表示描述一個頂點的總的size;STRIDE和以前一樣,表示跨距,它們的值已經很清楚了。接着來看loadBitmapData方法,因為我們在頂點緩沖區中增加了法線分量,是以需要在heightmapVertices中相應增加法線的存儲空間;兩個for循環的目的還是為了填充頂點數組,getPoint方法的注釋已經寫的非常清楚了,傳回由row和col給出的預期位置處的一個點,它的Y分量和我們上一節講的一樣,就是取該點處的pixel色值的紅色分量進行歸一化的結果,該方法中還進行了邊界判斷,比如我們高度圖左、上、右、下四條邊界,它們再往外就是空的,是以需要進行判斷。這裡大家一定要清楚,我們計算這些都是為了得到該平面的法線,必須得到法線,才能計算餘弦值,進而得到朗伯體反射,這是我們最終的目的!!為了獲得法線,我們先取該點的left、top、right、bottom這些相鄰點(示例圖如下),建立其平面的兩個向量,這裡一定要注意順序,是從右向左,從上到下;然後調用Vector類的crossProduct方法得到叉積,并進行歸一化就得到表面法線了。

Opengl ES系列學習--點亮世界

     得到法線向量後,将其存儲在頂點數組中;clamp方法是為了進行邊界限定,如果val值比min還小,那麼就傳回min;如果它比max還大,那麼就傳回max;最後看一下bindData方法,我們增加了法線,是以着色器中也一定要定義法線屬性,它的值是從頂點緩沖區中取到的,是以定義法線時,肯定是attribute,而不是uniform,相信大家在清楚了前面幾節的基礎上,了解這些應該非常容易了;也因為增加了法線,是以頂點數組中不再是單純的位置,是以stride跨距也就是必須的了,而且調用setVertexAttribPointer給法線指派時,我們還必須告訴它offset偏移量,否則取值會出錯!

     接着看HeightmapShaderProgram類中的代碼,源碼如下:

public class HeightmapShaderProgram extends ShaderProgram {           
    private final int uVectorToLightLocation;
    private final int uMVMatrixLocation;
    private final int uIT_MVMatrixLocation;
    private final int uMVPMatrixLocation;
    private final int uPointLightPositionsLocation;
    private final int uPointLightColorsLocation;
    
    private final int aPositionLocation;
    private final int aNormalLocation;

    public HeightmapShaderProgram(Context context) {
        super(context, R.raw.heightmap_vertex_shader,
            R.raw.heightmap_fragment_shader);
                
        uVectorToLightLocation = glGetUniformLocation(program, U_VECTOR_TO_LIGHT);
        uMVMatrixLocation = glGetUniformLocation(program, U_MV_MATRIX);
        uIT_MVMatrixLocation = glGetUniformLocation(program, U_IT_MV_MATRIX);
        uMVPMatrixLocation = glGetUniformLocation(program, U_MVP_MATRIX);
        
        uPointLightPositionsLocation = 
            glGetUniformLocation(program, U_POINT_LIGHT_POSITIONS);
        uPointLightColorsLocation = 
            glGetUniformLocation(program, U_POINT_LIGHT_COLORS);
        aPositionLocation = glGetAttribLocation(program, A_POSITION);
        aNormalLocation = glGetAttribLocation(program, A_NORMAL);
    }

    /*
    public void setUniforms(float[] matrix, Vector vectorToLight) {
        glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);   
        glUniform3f(uVectorToLightLocation, 
            vectorToLight.x, vectorToLight.y, vectorToLight.z);
    }
     */
    
    public void setUniforms(float[] mvMatrix, 
                            float[] it_mvMatrix, 
                            float[] mvpMatrix, 
                            float[] vectorToDirectionalLight, 
                            float[] pointLightPositions,
                            float[] pointLightColors) {          
        glUniformMatrix4fv(uMVMatrixLocation, 1, false, mvMatrix, 0);   
        glUniformMatrix4fv(uIT_MVMatrixLocation, 1, false, it_mvMatrix, 0);   
        glUniformMatrix4fv(uMVPMatrixLocation, 1, false, mvpMatrix, 0);
        glUniform3fv(uVectorToLightLocation, 1, vectorToDirectionalLight, 0);
        
        glUniform4fv(uPointLightPositionsLocation, 3, pointLightPositions, 0);            
        glUniform3fv(uPointLightColorsLocation, 3, pointLightColors, 0);
    }

    public int getPositionAttributeLocation() {
        return aPositionLocation;
    }
    
    public int getNormalAttributeLocation() {
        return aNormalLocation;
    }
}
           

     這是修改後的高度圖着色器,基本也很容易了解了,構造方法中對我們定義的所有uniform、attribute類型的變量進行取值,友善後續傳值使用;setUniforms方法中從Render中傳參,然後指派給對應的變量,以實作對着色器程式的控制。這裡需要注意,我們設定uniform變量的值時,一定要注意對應的API,必須要和我們着色器中定義的類型相同,否則就會出錯,這已經是最基本的要求了!

     繼續看ParticlesRenderer類,源碼如下:

public class ParticlesRenderer implements Renderer {    
    private final Context context;

    private final float[] modelMatrix = new float[16];
    private final float[] viewMatrix = new float[16];
    private final float[] viewMatrixForSkybox = new float[16];
    private final float[] projectionMatrix = new float[16];        
    
    private final float[] tempMatrix = new float[16];
    private final float[] modelViewMatrix = new float[16];
    private final float[] it_modelViewMatrix = new float[16];
    private final float[] modelViewProjectionMatrix = new float[16];

    private HeightmapShaderProgram heightmapProgram;
    private Heightmap heightmap;
    
    /*
    private final Vector vectorToLight = new Vector(0.61f, 0.64f, -0.47f).normalize();
    */ 
    /*
    private final Vector vectorToLight = new Vector(0.30f, 0.35f, -0.89f).normalize();
    */
    final float[] vectorToLight = {0.30f, 0.35f, -0.89f, 0f};
    
    private final float[] pointLightPositions = new float[]
        {-1f, 1f, 0f, 1f,
          0f, 1f, 0f, 1f,
          1f, 1f, 0f, 1f};
    
    private final float[] pointLightColors = new float[]
        {1.00f, 0.20f, 0.02f,
         0.02f, 0.25f, 0.02f, 
         0.02f, 0.20f, 1.00f};
    
    private SkyboxShaderProgram skyboxProgram;
    private Skybox skybox;   
    
    private ParticleShaderProgram particleProgram;      
    private ParticleSystem particleSystem;
    private ParticleShooter redParticleShooter;
    private ParticleShooter greenParticleShooter;
    private ParticleShooter blueParticleShooter;     

    private long globalStartTime;    
    private int particleTexture;
    private int skyboxTexture;
    
    private float xRotation, yRotation;  

    public ParticlesRenderer(Context context) {
        this.context = context;
    }

    public void handleTouchDrag(float deltaX, float deltaY) {
        xRotation += deltaX / 16f;
        yRotation += deltaY / 16f;
        
        if (yRotation < -90) {
            yRotation = -90;
        } else if (yRotation > 90) {
            yRotation = 90;
        } 
        
        // Setup view matrix
        updateViewMatrices();        
    }
    
    private void updateViewMatrices() {        
        setIdentityM(viewMatrix, 0);
        rotateM(viewMatrix, 0, -yRotation, 1f, 0f, 0f);
        rotateM(viewMatrix, 0, -xRotation, 0f, 1f, 0f);
        System.arraycopy(viewMatrix, 0, viewMatrixForSkybox, 0, viewMatrix.length);
        
        // We want the translation to apply to the regular view matrix, and not
        // the skybox.
        translateM(viewMatrix, 0, 0, -1.5f, -5f);

//        // This helps us figure out the vector for the sun or the moon.        
//        final float[] tempVec = {0f, 0f, -1f, 1f};
//        final float[] tempVec2 = new float[4];
//        
//        Matrix.multiplyMV(tempVec2, 0, viewMatrixForSkybox, 0, tempVec, 0);
//        Log.v("Testing", Arrays.toString(tempVec2));
    }  

    @Override
    public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);  
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_CULL_FACE);
        
        heightmapProgram = new HeightmapShaderProgram(context);
        heightmap = new Heightmap(((BitmapDrawable)context.getResources()
            .getDrawable(R.drawable.heightmap)).getBitmap());
        
        skyboxProgram = new SkyboxShaderProgram(context);
        skybox = new Skybox();      
        
        particleProgram = new ParticleShaderProgram(context);        
        particleSystem = new ParticleSystem(10000);        
        globalStartTime = System.nanoTime();
        
        final Vector particleDirection = new Vector(0f, 0.5f, 0f);              
        final float angleVarianceInDegrees = 5f; 
        final float speedVariance = 1f;
            
        redParticleShooter = new ParticleShooter(
            new Point(-1f, 0f, 0f), 
            particleDirection,                
            Color.rgb(255, 50, 5),            
            angleVarianceInDegrees, 
            speedVariance);
        
        greenParticleShooter = new ParticleShooter(
            new Point(0f, 0f, 0f), 
            particleDirection,
            Color.rgb(25, 255, 25),            
            angleVarianceInDegrees, 
            speedVariance);
        
        blueParticleShooter = new ParticleShooter(
            new Point(1f, 0f, 0f), 
            particleDirection,
            Color.rgb(5, 50, 255),            
            angleVarianceInDegrees, 
            speedVariance); 
                
        particleTexture = TextureHelper.loadTexture(context, R.drawable.particle_texture);
        
//        skyboxTexture = TextureHelper.loadCubeMap(context, 
//            new int[] { R.drawable.left, R.drawable.right,
//                        R.drawable.bottom, R.drawable.top, 
//                        R.drawable.front, R.drawable.back}); 
        skyboxTexture = TextureHelper.loadCubeMap(context, 
        new int[] { R.drawable.night_left, R.drawable.night_right,
                    R.drawable.night_bottom, R.drawable.night_top, 
                    R.drawable.night_front, R.drawable.night_back});
    }

    @Override
    public void onSurfaceChanged(GL10 glUnused, int width, int height) {                
        glViewport(0, 0, width, height);        

        MatrixHelper.perspectiveM(projectionMatrix, 45, (float) width
            / (float) height, 1f, 100f);   
        updateViewMatrices();
    }

    @Override    
    public void onDrawFrame(GL10 glUnused) {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);                
                        
        drawHeightmap();
        drawSkybox();        
        drawParticles();
    }

    private void drawHeightmap() {
        setIdentityM(modelMatrix, 0);  
        
        // Expand the heightmap's dimensions, but don't expand the height as
        // much so that we don't get insanely tall mountains.        
        scaleM(modelMatrix, 0, 100f, 10f, 100f);
        updateMvpMatrix();        
        
        heightmapProgram.useProgram();
        /*
        heightmapProgram.setUniforms(modelViewProjectionMatrix, vectorToLight);
         */
        
        // Put the light positions into eye space.        
        final float[] vectorToLightInEyeSpace = new float[4];
        final float[] pointPositionsInEyeSpace = new float[12];                
        multiplyMV(vectorToLightInEyeSpace, 0, viewMatrix, 0, vectorToLight, 0);
        multiplyMV(pointPositionsInEyeSpace, 0, viewMatrix, 0, pointLightPositions, 0);
        multiplyMV(pointPositionsInEyeSpace, 4, viewMatrix, 0, pointLightPositions, 4);
        multiplyMV(pointPositionsInEyeSpace, 8, viewMatrix, 0, pointLightPositions, 8); 
        
        heightmapProgram.setUniforms(modelViewMatrix, it_modelViewMatrix, 
            modelViewProjectionMatrix, vectorToLightInEyeSpace,
            pointPositionsInEyeSpace, pointLightColors);
        heightmap.bindData(heightmapProgram);
        heightmap.draw(); 
    }
    
    private void drawSkybox() {   
        setIdentityM(modelMatrix, 0);
        updateMvpMatrixForSkybox();
                
        glDepthFunc(GL_LEQUAL); // This avoids problems with the skybox itself getting clipped.
        skyboxProgram.useProgram();
        skyboxProgram.setUniforms(modelViewProjectionMatrix, skyboxTexture);
        skybox.bindData(skyboxProgram);
        skybox.draw();
        glDepthFunc(GL_LESS);
    }
   
    private void drawParticles() {        
        float currentTime = (System.nanoTime() - globalStartTime) / 1000000000f;
        
        redParticleShooter.addParticles(particleSystem, currentTime, 1);
        greenParticleShooter.addParticles(particleSystem, currentTime, 1);              
        blueParticleShooter.addParticles(particleSystem, currentTime, 1);              
        
        setIdentityM(modelMatrix, 0);
        updateMvpMatrix();
        
        glDepthMask(false);
        glEnable(GL_BLEND);
        glBlendFunc(GL_ONE, GL_ONE);
        
        particleProgram.useProgram();
        particleProgram.setUniforms(modelViewProjectionMatrix, currentTime, particleTexture);
        particleSystem.bindData(particleProgram);
        particleSystem.draw(); 
        
        glDisable(GL_BLEND);
        glDepthMask(true);
    }
    
    /*
    private void updateMvpMatrix() {
        multiplyMM(tempMatrix, 0, viewMatrix, 0, modelMatrix, 0);        
        multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, tempMatrix, 0);
    }
    */
    
    private void updateMvpMatrix() {
        multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0);
        invertM(tempMatrix, 0, modelViewMatrix, 0);
        transposeM(it_modelViewMatrix, 0, tempMatrix, 0);        
        multiplyMM(
            modelViewProjectionMatrix, 0, 
            projectionMatrix, 0, 
            modelViewMatrix, 0);
    }
        
    private void updateMvpMatrixForSkybox() {
        multiplyMM(tempMatrix, 0, viewMatrixForSkybox, 0, modelMatrix, 0);
        multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, tempMatrix, 0);
    }
}
           

     該類相比之前,不斷的增加,現在又新增了modelViewMatrix、it_modelViewMatrix兩個矩陣,它們就是為了實作我們本節一開始所講的第三個概念使用點光源的第三個描述而添加的。它們的指派是在updateMvpMatrix方法當中,我們後面再分析它;接着定義了vectorToLight,它表示方向光,它為什麼要定義成四位呢?因為矩陣運算的規則是這樣的,我們要把方向光放入眼空間中,需要将它和矩陣進行運算,還是參考本節前面描述的第三個概念。

Opengl ES系列學習--點亮世界

     接下來的pointLightPositions表示給三個粒子發射器添加的點光源的位置,Y分量比粒子發射器的位置都大1,以免被遮住,W分量為1,pointLightColors表示三個點光源的顔色;接下來的updateViewMatrices方法和上一節完全相同,onSurfaceCreated方法中和上一節的差異隻是将天空盒換成了一個黑夜的天空盒;onSurfaceChanged、onDrawFrame方法完全相同。

     drawHeightmap實作高度圖的繪制,因為我們在HeightmapShaderProgram類中定義的setUniforms預留的參數,是以需要計算出這幾個參數,傳遞進去。multiplyMV(vectorToLightInEyeSpace, 0, viewMatrix, 0, vectorToLight, 0)将視圖矩陣viewMatrix和方向光向量vectorToLight相乘,結果存儲在vectorToLightInEyeSpace向量當中,因為前面已經調用了updateMvpMatrix方法,該方法中已經将視圖矩陣和模型矩陣相乘了,是以updateMvpMatrix方法執行完,方向光已經在世界空間中了,是以這裡計算完的結果,方向光向量已經在眼空間中了;緊接着的三個multiplyMV原理完全相同,作用是将三個點光源放入眼空間中,三個點光源全部都占四位,是以offset偏移值為4,三個點光源向量的結果全部存儲在pointPositionsInEyeSpace數組中;計算完成之後,調用heightmapProgram.setUniforms将參數設定到着色器程式中。

     該類中最後來看一下updateMvpMatrix方法的實作,先調用multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0)将視圖矩陣和模型矩陣相乘,得到的就是我們本節最上面三個概念中講的模型視圖矩陣;然後調用invertM(tempMatrix, 0, modelViewMatrix, 0)将模型視圖矩陣反轉;再調用transposeM(it_modelViewMatrix, 0, tempMatrix, 0)對反轉後的矩陣進行倒置,這幾步完全都是按照我們之前的幾個概念來實作的,是以還是要先搞清楚概念,否則我們根本不知道下一步要怎麼處理。

     最後來看一下頂點着色器和片段着色器,頂點着色器的源碼如下:

// uniform mat4 u_Matrix;
uniform mat4 u_MVMatrix;
uniform mat4 u_IT_MVMatrix;
uniform mat4 u_MVPMatrix;


uniform vec3 u_VectorToLight;             // In eye space
/*
uniform vec3 u_VectorToLight;
*/

uniform vec4 u_PointLightPositions[3];    // In eye space
uniform vec3 u_PointLightColors[3];


attribute vec4 a_Position;
attribute vec3 a_Normal;


varying vec3 v_Color;
vec3 materialColor;
vec4 eyeSpacePosition;
vec3 eyeSpaceNormal;

vec3 getAmbientLighting();
vec3 getDirectionalLighting();
vec3 getPointLighting();
void main()                    
{    
    /*
    v_Color = mix(vec3(0.180, 0.467, 0.153),    // A dark green 
                  vec3(0.660, 0.670, 0.680),    // A stony gray 
                  a_Position.y);                      
    
    // Note: The lighting code here doesn't take into account any rotations/
    // translations/etc... that may have been done to the model. In that case, 
    // the combined model-view matrix should be passed in, and the normals 
    // transformed with that matrix (if the matrix contains any skew / scale, 
    // then the normals will need to be multiplied by the inverse transpose 
    // (see http://arcsynthesis.org/gltut/Illumination/Tut09%20Normal%20Transformation.html
    // for more information)).
    
    vec3 scaledNormal = a_Normal;
    scaledNormal.y *= 10.0;
    scaledNormal = normalize(scaledNormal);
    
    float diffuse = max(dot(scaledNormal, u_VectorToLight), 0.0);
    
    diffuse *= 0.3;
    
    v_Color *= diffuse;    
                    
    float ambient = 0.2;
        
    float ambient = 0.1;
    
    v_Color += ambient;
    */                    
    
    materialColor = mix(vec3(0.180, 0.467, 0.153),    // A dark green 
                        vec3(0.660, 0.670, 0.680),    // A stony gray 
                        a_Position.y);
    eyeSpacePosition = u_MVMatrix * a_Position;                             
                             
    // The model normals need to be adjusted as per the transpose 
    // of the inverse of the modelview matrix.    
    eyeSpaceNormal = normalize(vec3(u_IT_MVMatrix * vec4(a_Normal, 0.0)));   
                                                                                  
    v_Color = getAmbientLighting();
    v_Color += getDirectionalLighting();                                                                                                                  
    v_Color += getPointLighting();
        
    gl_Position = u_MVPMatrix * a_Position;
}  

vec3 getAmbientLighting() 
{    
    return materialColor * 0.1;      
}

vec3 getDirectionalLighting()
{   
    return materialColor * 0.3 
         * max(dot(eyeSpaceNormal, u_VectorToLight), 0.0);       
}

vec3 getPointLighting()
{
    vec3 lightingSum = vec3(0.0);
    
    for (int i = 0; i < 3; i++) {                  
        vec3 toPointLight = vec3(u_PointLightPositions[i]) 
                          - vec3(eyeSpacePosition);          
        float distance = length(toPointLight);
        toPointLight = normalize(toPointLight);
        
        float cosine = max(dot(eyeSpaceNormal, toPointLight), 0.0); 
        lightingSum += (materialColor * u_PointLightColors[i] * 5.0 * cosine) 
                       / distance;
    }  
    
    return lightingSum;       
}
  
           

     開頭定義了三個矩陣,它們的值都是在Render中調用heightmapProgram.setUniforms傳進來的,u_MVMatrix表示模型視圖矩陣,u_IT_MVMatrix表示倒置矩陣的轉置,u_MVPMatrix表示合并後的模型視圖投影矩陣,它的作用就和我們之前使用的透視投影矩陣的作用完全相同;u_VectorToLight表示方向光;u_PointLightPositions[3]表示三個點光源的位置屬性;u_PointLightColors[3]表示三個點光源的顔色屬性;a_Position表示高度圖頂點屬性;a_Normal表示法線,它的值就是在Heightmap類的loadBitmapData方法中給頂點數組指派時計算得到的,詳細的計算過程我們前面已經詳細講解了;materialColor表示材質顔色,其實也就是高度圖表面顔色;eyeSpacePosition表示高度圖頂點在眼空間中的位置;eyeSpaceNormal表示法線在眼空間中的位置。

     main函數中第一句還是調用mix方法根據Y分量值擷取高度圖的顔色插值,然後調用eyeSpacePosition = u_MVMatrix * a_Position将高度圖的頂點位置轉換成眼空間中的位置,因為我們需要計算每個點光源與目前位置的向量,是以這裡需要轉換,後邊就用到它了;接着将法線與轉置矩陣相乘并歸一化,得到法線在眼空間中的向量,這些都是根據前面講的概念來實作的;然後分别調用getAmbientLighting()計算環境光,調用getDirectionalLighting()計算方向光的朗伯體反射,調用getPointLighting()計算三個點光源的朗伯體反射,把是以結果累加得到要傳遞給片段着色器的v_Color的最終值;最後調用gl_Position = u_MVPMatrix * a_Position給内建變量gl_Position指派。getAmbientLighting方法很簡單,就是把光照強度減小,系數0.1沒有任何意義,大家可以自己修改;getDirectionalLighting方法的目的就是計算方向光的朗伯體反射,我們在本節開頭已經用它作為示例講解過了;getPointLighting是用來計算點光源的朗伯體反射,為什麼for循環要執行三次呢?因為我們定義的點光源有三個,每個點光源都會影響一個高度圖的效果,是以需要把三個點光源的影響全部計算在内。計算過程還是依照前面講的概念,對點光源,需要先計算出光源到頂點位置的向量,然後調用normalize将向量歸一化,再和法線運算取點積得到餘弦值,也就是朗伯體反射的系數,最終和表面光materialColor相乘就得到朗伯體反射的最終結果了。這裡将它和距離distance相除,是為了仿照光照密度随距離遞減的效果,而乘5.0是為了放大光源效果,5.0也沒有任何意義,大家可以随意修改它,就可以看到光強度明顯變化了。

     片段着色器和之前一樣,很簡單,我們就不展開分析了。

     看完本節的分析,大家是不是最終就明白了,其實光照效果最終還是作用在顔色上來實作的,就和我們之前講的速度一樣,它是通過位移變化來實作的,這些抽象的東西本身是無法描述的,我們必須把它落實在實際的像素點上,這樣才能非常準确的量化它們。

     好了,本節還涉及很多概念性的知識,了解好這些概念也非常重要,如果我們不懂這些概念,那讓我們自己寫,根本寫不出來,因為我們根本不知道如何去計算它們,是以還需要用心掌握。

     下課!!!