当前位置: 首页 > news >正文

Android 使用 OpenGL ES 绘制球面

公众号回复:OpenGL,领取学习资源大礼包

作者:cain_huang

来源:https://www.jianshu.com/p/c127387cd504

新媒体排版

关于绘制球面,我们首先需要了解的是,这么去构建一个球面纹理。

要构建球面,我们首先需要知道的顶点和纹理需要怎么构建。

球面纹理一般情况下是通过将球面拆分成多个平面,然后使用三角形对平面进行构建。

当拆分成的平面越多,则约像一个球。

球面的顶点一般都是相互连接起来的,如下图所示:

球面顶点.png

既然知道了球面顶点的连接方式,那么该如何计算球面顶点的位置?

如下图所示:

三维坐标公式.png

根据三角形公式可以得到:

x0 = R * cos(a) * sin(b);
y0 = R * sin(a);
z0 = R * cos(a) * cos(b);

我们可以由此得到一个球某个位置的顶点所在的位置。那么我们可以通过将球面拆分成许许多多的按照一定值的夹角的顶点,相互连接起来,就构成了我们所需要的球。

计算顶点方法如下:

    for (double vAngle = 0; vAngle < Math.PI; vAngle = vAngle + angleSpan) { // vertical
            for (double hAngle = 0; hAngle < 2 * Math.PI; hAngle = hAngle + angleSpan) { // horizontal

                float x0 = (float) (radius * Math.sin(vAngle) * Math.cos(hAngle));
                float y0 = (float) (radius * Math.sin(vAngle) * Math.sin(hAngle));
                float z0 = (float) (radius * Math.cos((vAngle)));

                float x1 = (float) (radius * Math.sin(vAngle) * Math.cos(hAngle + angleSpan));
                float y1 = (float) (radius * Math.sin(vAngle) * Math.sin(hAngle + angleSpan));
                float z1 = (float) (radius * Math.cos(vAngle));

                float x2 = (float) (radius * Math.sin(vAngle + angleSpan) * Math.cos(hAngle + angleSpan));
                float y2 = (float) (radius * Math.sin(vAngle + angleSpan) * Math.sin(hAngle + angleSpan));
                float z2 = (float) (radius * Math.cos(vAngle + angleSpan));

                float x3 = (float) (radius * Math.sin(vAngle + angleSpan) * Math.cos(hAngle));
                float y3 = (float) (radius * Math.sin(vAngle + angleSpan) * Math.sin(hAngle));
                float z3 = (float) (radius * Math.cos(vAngle + angleSpan));

                vertex.add(x1);
                vertex.add(y1);
                vertex.add(z1);

                vertex.add(x3);
                vertex.add(y3);
                vertex.add(z3);

                vertex.add(x0);
                vertex.add(y0);
                vertex.add(z0);

                vertex.add(x1);
                vertex.add(y1);
                vertex.add(z1);

                vertex.add(x2);
                vertex.add(y2);
                vertex.add(z2);

                vertex.add(x3);
                vertex.add(y3);
                vertex.add(z3);
            }
        }

上面的方式就是通过横向和纵向按照一定的角度分割成多个顶点,将所有的顶点相互连接成一小块平面后,拼接起来就得到了我们所需要的球面。

好了,我们来看看具体实现吧:

  • 自定义GLSurfaceView:

public class SphereSurfaceView extends GLSurfaceView {

    private SphereRender mSphereRender;

    public SphereSurfaceView(Context context) {
        super(context);
        init(context);
    }

    public SphereSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    private void init(Context context) {
        mSphereRender = new SphereRender(context);
        setEGLContextClientVersion(2);
        setRenderer(mSphereRender);
        setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
        setOnTouchListener(new OnTouchListener() {

            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN://检测到点击事件时
                        mSphereRender.rotate(20f, 0, 1, 0); //绕y轴旋转
                }
                return true;
            }
        });
    }
    
}

点击GLSurfaceView时,球面会绕Y轴旋转20度。

  • 我们来看看Render的实现:

public class SphereRender implements GLSurfaceView.Renderer {

    // 视图矩阵
    private float[] mViewMatrix = new float[16];
    // 模型矩阵
    private float[] mModelMatrix = new float[16];
    // 投影矩阵
    private float[] mProjectionMatrix = new float[16];
    // 总变换矩阵
    private float[] mMVPMatrix = new float[16];

    private SphereFilter mSphereFilter;

    private Context mContext;

    public SphereRender(Context context) {
        mContext = context;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //设置屏幕背景色RGBA
        GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
        //打开深度检测
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);
        //打开背面剪裁
        GLES20.glEnable(GLES20.GL_CULL_FACE);
        // 创建球面绘制实体
        mSphereFilter = new SphereFilter();
        // 初始化矩阵
        initMatrix();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
        float ratio = (float) width / height;
        // 调用此方法计算产生透视投影矩阵
        Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 20, 100);
        // 调用此方法产生摄像机9参数位置矩阵
        Matrix.setLookAtM(mViewMatrix, 0,
                0f, 0f, 30f,    // 相机位置
                0f, 0f, 0f,     // 目标位置
                0f, 1.0f, 0.0f  // 相机朝向
        );
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        //清除深度缓冲与颜色缓冲
        GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        calculateMatrix();
        mSphereFilter.setMVPMatrix(mMVPMatrix);
        mSphereFilter.drawSphere();
    }

    /**
     * 初始化矩阵
     */
    private void initMatrix() {
        Matrix.setIdentityM(mViewMatrix, 0);
        Matrix.setIdentityM(mModelMatrix, 0);
        Matrix.setIdentityM(mProjectionMatrix, 0);
        Matrix.setIdentityM(mMVPMatrix, 0);
    }

    /**
     * 计算总变换矩阵
     */
    private void calculateMatrix() {
        Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);
        Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);
    }

    /**
     * 物体旋转
     * @param angle
     * @param x
     * @param y
     * @param z
     */
    public void rotate(float angle, float x, float y, float z) {
        Matrix.rotateM(mModelMatrix, 0, angle, x, y, z);
    }
}

Render主要是启用了深度检测,然后设置相应的视图矩阵、投影矩阵和模型矩阵等,在绘制时计算出综合变换矩阵传递给球面绘制实体,然后绘制球面。

  • 球面绘制实体的实现如下:

public class SphereFilter {

    private static final String VERTEX_SHADER =
            "uniform mat4 u_Matrix;//最终的变换矩阵\n" +
            "attribute vec4 a_Position;//顶点位置\n" +
            "varying vec4 vPosition;//用于传递给片元着色器的顶点位置\n" +
            "void main() {\n" +
            "    gl_Position = u_Matrix * a_Position;\n" +
            "    vPosition = a_Position;\n" +
            "}";

    private static final String FRAGMENT_SHADER =
            "precision mediump float;\n" +
            "varying vec4 vPosition;\n" +
            "void main() {\n" +
            "    float uR = 0.6;//球的半径\n" +
            "    vec4 color;\n" +
            "    float n = 8.0;//分为n层n列n行\n" +
            "    float span = 2.0*uR/n;//正方形长度\n" +
            "    //计算行列层数\n" +
            "    int i = int((vPosition.x + uR)/span);//行数\n" +
            "    int j = int((vPosition.y + uR)/span);//层数\n" +
            "    int k = int((vPosition.z + uR)/span);//列数\n" +
            "    int colorType = int(mod(float(i+j+k),2.0));\n" +
            "    if(colorType == 1) {//奇数时为绿色\n" +
            "        color = vec4(0.2,1.0,0.129,0);\n" +
            "    } else {//偶数时为白色\n" +
            "        color = vec4(1.0,1.0,1.0,0);//白色\n" +
            "    }\n" +
            "    //将计算出的颜色给此片元\n" +
            "    gl_FragColor = color;\n" +
            "}";

    private float radius = 1.0f; // 球的半径
    final double angleSpan = Math.PI / 90f; // 将球进行单位切分的角度
    private FloatBuffer mVertexBuffer;// 顶点坐标
    int mVertexCount = 0;// 顶点个数,先初始化为0

    // float类型的字节数
    private static final int BYTES_PER_FLOAT = 4;

    // 数组中每个顶点的坐标数
    private static final int COORDS_PER_VERTEX = 3;

    private int mProgramHandle;
    private int muMatrixHandle;
    private int maPositionHandle;

    private float[] mMVPMatrix = new float[16];

    public SphereFilter() {
        initSphereVertex();
        createProgram();
        Matrix.setIdentityM(mMVPMatrix, 0);
    }

    /**
     * 计算球面顶点
     */
    public void initSphereVertex() {
        ArrayList<Float> vertex = new ArrayList<Float>();

        for (double vAngle = 0; vAngle < Math.PI; vAngle = vAngle + angleSpan) { // vertical
            for (double hAngle = 0; hAngle < 2 * Math.PI; hAngle = hAngle + angleSpan) { // horizontal

                float x0 = (float) (radius * Math.sin(vAngle) * Math.cos(hAngle));
                float y0 = (float) (radius * Math.sin(vAngle) * Math.sin(hAngle));
                float z0 = (float) (radius * Math.cos((vAngle)));

                float x1 = (float) (radius * Math.sin(vAngle) * Math.cos(hAngle + angleSpan));
                float y1 = (float) (radius * Math.sin(vAngle) * Math.sin(hAngle + angleSpan));
                float z1 = (float) (radius * Math.cos(vAngle));

                float x2 = (float) (radius * Math.sin(vAngle + angleSpan) * Math.cos(hAngle + angleSpan));
                float y2 = (float) (radius * Math.sin(vAngle + angleSpan) * Math.sin(hAngle + angleSpan));
                float z2 = (float) (radius * Math.cos(vAngle + angleSpan));

                float x3 = (float) (radius * Math.sin(vAngle + angleSpan) * Math.cos(hAngle));
                float y3 = (float) (radius * Math.sin(vAngle + angleSpan) * Math.sin(hAngle));
                float z3 = (float) (radius * Math.cos(vAngle + angleSpan));

                vertex.add(x1);
                vertex.add(y1);
                vertex.add(z1);

                vertex.add(x3);
                vertex.add(y3);
                vertex.add(z3);

                vertex.add(x0);
                vertex.add(y0);
                vertex.add(z0);

                vertex.add(x1);
                vertex.add(y1);
                vertex.add(z1);

                vertex.add(x2);
                vertex.add(y2);
                vertex.add(z2);

                vertex.add(x3);
                vertex.add(y3);
                vertex.add(z3);
            }
        }

        mVertexCount = vertex.size() / COORDS_PER_VERTEX;
        float vertices[] = new float[vertex.size()];
        for (int i = 0; i < vertex.size(); i++) {
            vertices[i] = vertex.get(i);
        }
        mVertexBuffer = GlUtil.createFloatBuffer(vertices);
    }

    /**
     * 创建Program
     */
    private void createProgram() {
        mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
        maPositionHandle = GLES20.glGetAttribLocation(mProgramHandle, "a_Position");
        muMatrixHandle = GLES20.glGetUniformLocation(mProgramHandle, "u_Matrix");
    }

    /**
     * 绘制球面
     */
    public void drawSphere() {
        GLES20.glUseProgram(mProgramHandle);
        GLES20.glVertexAttribPointer(maPositionHandle, COORDS_PER_VERTEX,
                GLES20.GL_FLOAT, false, 0, mVertexBuffer);
        GLES20.glEnableVertexAttribArray(maPositionHandle);
        GLES20.glUniformMatrix4fv(muMatrixHandle, 1, false, mMVPMatrix, 0);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, mVertexCount);
        GLES20.glDisableVertexAttribArray(maPositionHandle);
        GLES20.glUseProgram(0);
    }

    /**
     * 设置最终的变换矩阵
     * @param matrix
     */
    public void setMVPMatrix(float[] matrix) {
        mMVPMatrix = matrix;
    }
}

实现过程主要是创建Program、绑定对应的Attribute属性、设置总变换矩阵、绘制球面等。

  • 下面是工具,用于创建Program、创建mipmap纹理等方法的封装:

public class GlUtil {

    public static final String TAG = "GlUtil";

    // 从初始化失败
    public static final int GL_NOT_INIT = -1;

    // 单位矩阵
    public static final float[] IDENTITY_MATRIX;
    static {
        IDENTITY_MATRIX = new float[16];
        Matrix.setIdentityM(IDENTITY_MATRIX, 0);
    }

    private static final int SIZEOF_FLOAT = 4;

    private GlUtil() {}

    /**
     * 创建program
     * @param vertexSource
     * @param fragmentSource
     * @return
     */
    public static int createProgram(String vertexSource, String fragmentSource) {
        int vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertexSource);
        if (vertexShader == 0) {
            return 0;
        }
        int pixelShader = loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentSource);
        if (pixelShader == 0) {
            return 0;
        }

        int program = GLES30.glCreateProgram();
        checkGlError("glCreateProgram");
        if (program == 0) {
            Log.e(TAG, "Could not create program");
        }
        GLES30.glAttachShader(program, vertexShader);
        checkGlError("glAttachShader");
        GLES30.glAttachShader(program, pixelShader);
        checkGlError("glAttachShader");
        GLES30.glLinkProgram(program);
        int[] linkStatus = new int[1];
        GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkStatus, 0);
        if (linkStatus[0] != GLES30.GL_TRUE) {
            Log.e(TAG, "Could not link program: ");
            Log.e(TAG, GLES30.glGetProgramInfoLog(program));
            GLES30.glDeleteProgram(program);
            program = 0;
        }
        return program;
    }

    /**
     * 加载Shader
     * @param shaderType
     * @param source
     * @return
     */
    public static int loadShader(int shaderType, String source) {
        int shader = GLES30.glCreateShader(shaderType);
        checkGlError("glCreateShader type=" + shaderType);
        GLES30.glShaderSource(shader, source);
        GLES30.glCompileShader(shader);
        int[] compiled = new int[1];
        GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0);
        if (compiled[0] == 0) {
            Log.e(TAG, "Could not compile shader " + shaderType + ":");
            Log.e(TAG, " " + GLES30.glGetShaderInfoLog(shader));
            GLES30.glDeleteShader(shader);
            shader = 0;
        }
        return shader;
    }

    /**
     * 检查是否出错
     * @param op
     */
    public static void checkGlError(String op) {
        int error = GLES30.glGetError();
        if (error != GLES30.GL_NO_ERROR) {
            String msg = op + ": glError 0x" + Integer.toHexString(error);
            Log.e(TAG, msg);
            throw new RuntimeException(msg);
        }
    }

    /**
     * 创建FloatBuffer
     * @param coords
     * @return
     */
    public static FloatBuffer createFloatBuffer(float[] coords) {
        ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * SIZEOF_FLOAT);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer fb = bb.asFloatBuffer();
        fb.put(coords);
        fb.position(0);
        return fb;
    }

    /**
     * 创建FloatBuffer
     * @param data
     * @return
     */
    public static FloatBuffer createFloatBuffer(ArrayList<Float> data) {
        float[] coords = new float[data.size()];
        for (int i = 0; i < coords.length; i++){
            coords[i] = data.get(i);
        }
        return createFloatBuffer(coords);
    }

    /**
     * 创建Texture对象
     * @param textureType
     * @return
     */
    public static int createTextureObject(int textureType) {
        int[] textures = new int[1];
        GLES30.glGenTextures(1, textures, 0);
        GlUtil.checkGlError("glGenTextures");
        int textureId = textures[0];
        GLES30.glBindTexture(textureType, textureId);
        GlUtil.checkGlError("glBindTexture " + textureId);
        GLES30.glTexParameterf(textureType, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
        GLES30.glTexParameterf(textureType, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
        GLES30.glTexParameterf(textureType, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
        GLES30.glTexParameterf(textureType, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
        GlUtil.checkGlError("glTexParameter");
        return textureId;
    }

    /**
     * 创建Sampler2D的Framebuffer 和 Texture
     * @param frameBuffer
     * @param frameBufferTex
     * @param width
     * @param height
     */
    public static void createSampler2DFrameBuff(int[] frameBuffer, int[] frameBufferTex,
                                                int width, int height) {
        GLES30.glGenFramebuffers(1, frameBuffer, 0);
        GLES30.glGenTextures(1, frameBufferTex, 0);
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, frameBufferTex[0]);
        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, width, height, 0,
                GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);
        GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
        GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR);
        GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
        GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
        GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, frameBuffer[0]);
        GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0,
                GLES30.GL_TEXTURE_2D, frameBufferTex[0], 0);
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
        GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0);
        checkGlError("createCamFrameBuff");
    }

    /**
     * 加载mipmap纹理
     * @param bitmap bitmap图片
     * @return
     */
    public static int createTexture(Bitmap bitmap) {
        int[] texture = new int[1];
        if (bitmap != null && !bitmap.isRecycled()) {
            //生成纹理
            GLES30.glGenTextures(1, texture, 0);
            checkGlError("glGenTexture");
            //生成纹理
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texture[0]);
            //设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
            GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                    GLES30.GL_TEXTURE_MIN_FILTER,GLES30.GL_NEAREST);
            //设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
            GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                    GLES30.GL_TEXTURE_MAG_FILTER,GLES30.GL_LINEAR);
            //设置环绕方向S,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
            GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                    GLES30.GL_TEXTURE_WRAP_S,GLES30.GL_CLAMP_TO_EDGE);
            //设置环绕方向T,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
            GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                    GLES30.GL_TEXTURE_WRAP_T,GLES30.GL_CLAMP_TO_EDGE);
            //根据以上指定的参数,生成一个2D纹理
            GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0);
            return texture[0];
        }
        return 0;
    }

    /**
     * 创建OES类型的Texture
     * @return
     */
    public static int createOESTexture() {
        int[] texture = new int[1];

        GLES20.glGenTextures(1, texture, 0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);

        return texture[0];
    }

    /**
     * 加载mipmap纹理
     * @param context
     * @param name
     * @return
     */
    public static int loadMipmapTextureFromAssets(Context context, String name) {
        int[] textureHandle = new int[1];
        GLES30.glGenTextures(1, textureHandle, 0);
        if (textureHandle[0] != 0) {
            Bitmap bitmap = getImageFromAssetsFile(context, name);
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureHandle[0]);

            GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                    GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
            GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                    GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR);
            GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                    GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
            GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
                    GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
            GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0);
            bitmap.recycle();
        }
        if (textureHandle[0] == 0) {
            throw new RuntimeException("Error loading texture.");
        }

        return textureHandle[0];
    }

    /**
     * 加载Assets文件夹下的图片
     * @param context
     * @param fileName
     * @return
     */
    public static Bitmap getImageFromAssetsFile(Context context, String fileName) {
        Bitmap bitmap = null;
        AssetManager manager = context.getResources().getAssets();
        try {
            InputStream is = manager.open(fileName);
            bitmap = BitmapFactory.decodeStream(is);
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

至此,我们就可以看到一个球面了:


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

扫码关注公众号【音视频开发进阶】,一起学习多媒体音视频开发~~~

喜欢就点个「在看」吧 ▽

相关文章:

  • 【音视频连载-007】基础学习篇-SDL 播放 PCM 音频文件(上)
  • PBO是OpenGL最高效的像素拷贝方式吗?
  • 游戏开发入门(一)游戏发展史
  • 【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下)
  • memcpy速度太慢?掌握这个技术让内存拷贝效率成倍提升
  • DXOMark是如何评价音频质量的
  • 【每周一记-001】~~~
  • 【音视频连载-009】第二季 FFmpeg 打造简易播放器
  • 【每周一记-002】
  • iOS中使用OpenGL 实现增高功能
  • 五分钟用C++11实现Android系统的Handler机制
  • 从入门到进阶|如何基于WebRTC搭建一个视频会议
  • 【音视频连载-010】第二季 FFmpeg 日志打印
  • 如何将ijkplayer引入AS工程中进行二次开发
  • Android 性能优化必知必会
  • Bytom交易说明(账户管理模式)
  • es6要点
  • HTML中设置input等文本框为不可操作
  • java8-模拟hadoop
  • JavaScript 奇技淫巧
  • Javascript弹出层-初探
  • Laravel Telescope:优雅的应用调试工具
  • Spring框架之我见(三)——IOC、AOP
  • tensorflow学习笔记3——MNIST应用篇
  • 仿天猫超市收藏抛物线动画工具库
  • 关于Flux,Vuex,Redux的思考
  • 前端面试题总结
  • 如何在 Tornado 中实现 Middleware
  • 实习面试笔记
  • Prometheus VS InfluxDB
  • ​LeetCode解法汇总1276. 不浪费原料的汉堡制作方案
  • ​LeetCode解法汇总2182. 构造限制重复的字符串
  • ​软考-高级-系统架构设计师教程(清华第2版)【第20章 系统架构设计师论文写作要点(P717~728)-思维导图】​
  • (1/2)敏捷实践指南 Agile Practice Guide ([美] Project Management institute 著)
  • (html转换)StringEscapeUtils类的转义与反转义方法
  • (第一天)包装对象、作用域、创建对象
  • (二)fiber的基本认识
  • (附源码)php投票系统 毕业设计 121500
  • (附源码)springboot炼糖厂地磅全自动控制系统 毕业设计 341357
  • (六)激光线扫描-三维重建
  • (论文阅读11/100)Fast R-CNN
  • (转)C#开发微信门户及应用(1)--开始使用微信接口
  • (转)es进行聚合操作时提示Fielddata is disabled on text fields by default
  • (转)负载均衡,回话保持,cookie
  • (转)重识new
  • *Django中的Ajax 纯js的书写样式1
  • ./indexer: error while loading shared libraries: libmysqlclient.so.18: cannot open shared object fil
  • .NET 5.0正式发布,有什么功能特性(翻译)
  • .NET CORE Aws S3 使用
  • .net core 调用c dll_用C++生成一个简单的DLL文件VS2008
  • .Net Core/.Net6/.Net8 ,启动配置/Program.cs 配置
  • .NET Framework与.NET Framework SDK有什么不同?
  • .Net Web项目创建比较不错的参考文章
  • .NET(C#) Internals: as a developer, .net framework in my eyes
  • .netcore 如何获取系统中所有session_如何把百度推广中获取的线索(基木鱼,电话,百度商桥等)同步到企业微信或者企业CRM等企业营销系统中...