Android显示系统(08)- OpenGL ES - 图片拉伸

news2024/12/13 1:30:14

Android显示系统(02)- OpenGL ES - 概述
Android显示系统(03)- OpenGL ES - GLSurfaceView的使用
Android显示系统(04)- OpenGL ES - Shader绘制三角形
Android显示系统(05)- OpenGL ES - Shader绘制三角形(使用glsl文件)
Android显示系统(06)- OpenGL ES - VBO和EBO和VAO
Android显示系统(07)- OpenGL ES - 纹理Texture
Android显示系统(08)- OpenGL ES - 图片拉伸

一、前言:

前面介绍了OpenGL当中通过纹理渲染图片的方法,但是,渲染出来的图片明显被拉伸,本文介绍下如何对抗这种拉伸。

二、防止被拉伸的方法:

1、什么是拉伸:

在这里插入图片描述

如上图所示,我本来要笑得很开心,就像左边图片一样,结果笑抽了,像右边一样了。这里面有个视口的概念,视口是指在屏幕上用于显示图形的矩形区域。

2、如何避免:

我们发现上图被拉伸是因为默认情况下视口大小就是屏幕大小,所以图片宽高比和屏幕宽高比不一样肯定就会被拉伸了。所以,我们将视口的宽高和照片保持一致是不是就可以正常显示了?这样的确可以解决问题,但是会存在一种情况,就是,如果照片很小,就像下图一样了:

在这里插入图片描述

是不是非常奇怪?因此,我们一般的做法就是:根据宽高比,选择宽顶到屏幕两头,或者长顶到屏幕两头!

具体调整方法:视口调整或者投影调整,下面详细分析:

三、视口调整:

注意,下图中视口的红线其实和照片一样大,为了让大家看清,留了一点空白。

在这里插入图片描述

最终结果就如上图所示。步骤如下:

  • 计算视口的宽高比:

    以图片宽高比和窗口宽高比为基础来决定视口的大小。首先计算宽高比:

    float textureWidth = 300.0f;
    float textureHeight = 200.0f;
    float windowWidth = 640.0f;
    float windowHeight = 480.0f;
    
    float textureAspectRatio = (float) textureWidth / textureHeight;
    float windowAspectRatio = (float) windowWidth / windowHeight;
    
  • 决定视口的大小:

    基于比较这两个宽高比,设置适合的视口:

    • 情况 1:窗口宽高比大于图片宽高比

      在这种情况下,窗口是较宽的,使用图片的高度为基准:

      if (windowAspectRatio > textureAspectRatio) {
          // 使用窗口高度作为基准
          float adjustedWidth = textureAspectRatio * windowHeight; // 调整后的宽度
          int viewportX = (windowWidth - adjustedWidth) / 2; // 左侧留白
          glViewport(viewportX, 0, adjustedWidth, windowHeight); // 设置视口
      } else {
          // 情况2
      }
      
    • 情况 2:窗口宽高比小于图片宽高比

      在这种情况下,窗口是较窄的,使用图片的宽度为基准:

      else {
          // 使用窗口宽度作为基准
          float adjustedHeight = (1.0f / textureAspectRatio) * windowWidth; // 调整后的高度
          int viewportY = (windowHeight - adjustedHeight) / 2; // 上侧留白
          glViewport(0, viewportY, windowWidth, adjustedHeight); // 设置视口
      }
      

在Android中我们有了图片的起始位置(左上角坐标)和宽高就可以调用下面函数完成视口调整:

glViewport(X, Y, vWidth, vHeight)

四、投影调整:

1、定义:

我们之前说了,在渲染管线的投影变换阶段有两种选择:正交变换透视变换,其中透视变换一般用于显示3D效果,物体距离观察者越远,尺寸越小,造成深度感,一般适用于电影、游戏等场景;而正交变换一般用于显示2D图片,无论距离如何,物体的尺寸保持不变,因此我们选择正交变换

  • 透视投影:

    在这里插入图片描述

  • 正交投影:

    在这里插入图片描述

2、正交投影特点:

  • 无失真:物体的各个部分间的相对关系保持不变。
  • 平行线:在投影过程中,平行线在投影后仍然平行。
  • 均匀缩放:物体在距离视点的远近时大小不会变化。

3、投影平面:

通常选择Z=0平面作为投影平面(Z轴等于0,也就是说三维空间中变成二维的XY平面)。

4、正交投影的过程:

在这里插入图片描述

可以看出,先平移到坐标系的远点和物体模型的几何中心重合,再缩放成单位立方体,最后将3D模型投影到2D屏幕上。也就是平移和缩放两步。

之前章节介绍过,投影过程中需要三个矩阵,我们一般叫做MVP,MVP 是指 Model-View-Projection(模型-视图-投影)矩阵,是一种常用的矩阵变换组合,用于将三维场景中的对象坐标转换为最终在二维屏幕上呈现的坐标。

  • Model Matrix(模型矩阵):用于将对象从局部坐标系转换到世界坐标系(即将对象放置到世界空间中的合适位置)。
  • View Matrix(视图矩阵):描述了观察者的位置和方向,将世界坐标系中的对象转换到观察者的视角坐标系。视图矩阵实际上是观察者逆向移动的矩阵。
  • Projection Matrix(投影矩阵):用于将观察坐标系中的对象投影到屏幕坐标系,定义了视锥体的几何形状,包括透视投影和正交投影。

一般情况下M我们不设置,或者设置为单位矩阵即可。V和P是需要我们计算出来的。

5、视图矩阵(V):

要构建相机的视图矩阵,以确定观察者的位置和方向。这个方法通常与 OpenGL ES 中的相机视图转换相关联。

主要通过 Matrix.setLookAtM() 方法完成:

  • 方法签名

    public static void setLookAtM(float[] rm, int rmOffset,
                                   float eyeX, float eyeY, float eyeZ,
                                   float centerX, float centerY, float centerZ,
                                   float upX, float upY, float upZ)
    
  • 参数

    • rm:目标矩阵,即要设置视图矩阵的目标数组。
    • rmOffset:目标矩阵的偏移量。
    • eyeX, eyeY, eyeZ:摄像机看向哪儿。
    • centerX, centerY, centerZ:摄像机观察的目标位置的坐标。
    • upX, upY, upZ:摄像机的朝向向量。
  • 功能setLookAtM() 方法的作用是根据摄像机的位置、观察目标的位置和朝向向量,设置视图矩阵。这个视图矩阵描述了一个摄像机从指定位置观看指定目标的视角。

  • 使用示例:下面是一个简单的示例代码,演示如何使用 Matrix.setLookAtM() 方法设置视图矩阵:

    float[] viewMatrix = new float[16];
    Matrix.setLookAtM(viewMatrix, 0,
                      eyeX, eyeY, eyeZ,
                      centerX, centerY, centerZ,
                      upX, upY, upZ);
    

6、投影矩阵(P):

正交投影矩阵用于定义如何将三维坐标转换为二维坐标。设定参数包括视口的左、右、下、上、近、远平面值。通用格式如下:

在这里插入图片描述

  • L (Left): 左侧边界
  • R (Right): 右侧边界
  • B (Bottom): 下侧边界
  • T (Top): 上侧边界
  • N (Near): 近剪裁平面
  • F (Far): 远剪裁平面

上面的矩阵如此负责,我们平时怎么记住,或者怎么构建呢?在Android中提供了如下方法,我们调用一下就会生成正交投影矩阵:

Matrix.othroM(mvpM, offset, l, r, b, t, n, f)
  • mvpM:目标数组,用于存储生成的正交投影矩阵。
  • offset:目标数组中的偏移量,指定从哪个位置开始存储矩阵数据,一般设置为0。
  • l, r, b, t:定义了正交投影矩阵的左、右、底和顶面边界。
  • n:近裁剪面(near clipping plane)的距离,即视锥体近端的距离。
  • f:远裁剪面(far clipping plane)的距离,即视锥体远端的距离。

由于图片是二维的,所以nf不会改变,我们将它们设置为-1和1即可。

对于宽高来说,则需要根据实际情况来设定。如果上下留白,则左右设置为-1, 1;如果左右留白,则上下设置为-1,1。

所以,我们又像视口变换一样,来到一张二维图片了,根据,宽高比分类讨论。

矩阵宽高计算算法

  • 当图片宽高比大于窗口宽高比
    • 这意味着图片更“宽”相对于窗口。例如,窗口是竖直的,而图片是横向的。
    • 计算模型的高度(tb)时,用图片的宽高比除以窗口的宽高比:tb = (WI/HI) / (WW/HW)
    • 设置正交投影矩阵的上边界 (t) 和下边界 (b) 为 -tbtb。这确保了在Y轴方向上,图片能够适当缩放以不失真。
  • 当图片宽高比小于或等于窗口宽高比
    • 这意味着图片更“高”相对于窗口。例如,窗口是横向的,而图片是竖直的。
    • 计算模型的宽度(lr):lr = (WW/HW) / (WI/HI) ;
    • 设置正交投影矩阵的左边界 (l) 和右边界 (r) 为 -lrlr。这确保了在X轴方向上,图片能够适当缩放以不失真。

这样我们就得到了正确的正交变换矩阵,之后用这个正交变换矩阵就可以将图片正确的渲染到屏幕上。

7、MVP生成:

Matrix.multiplyMM() 是 Android 提供的用于矩阵相乘的方法,用于将两个 4x4 的浮点数矩阵相乘,并将结果存储在一个目标矩阵中。这个方法通常用于进行矩阵变换,比如将模型矩阵、视图矩阵和投影矩阵相乘,以生成 MVP(Model-View-Projection)矩阵。

以下是关于 Matrix.multiplyMM() 方法的一些重要信息:

  • 方法签名

    public static void multiplyMM(float[] result, int resultOffset,
                                   float[] lhs, int lhsOffset, float[] rhs, int rhsOffset)
    
  • 参数

    • result:存储结果的目标矩阵数组。
    • resultOffset:目标矩阵的偏移量。
    • lhs:左矩阵数组(第一个矩阵)。
    • lhsOffset:左矩阵的偏移量。
    • rhs:右矩阵数组(第二个矩阵)。
    • rhsOffset:右矩阵的偏移量。
  • 功能

    multiplyMM() 方法将两个 4x4 的浮点数矩阵相乘,即左矩阵和右矩阵相乘,结果存储在目标矩阵中。这种矩阵相乘通常用于进行复合变换,例如将模型矩阵、视图矩阵和投影矩阵相乘。

  • 使用示例:下面是一个简单的示例代码,演示如何使用 Matrix.multiplyMM() 方法进行矩阵相乘:

    float[] resultMatrix = new float[16];
    Matrix.multiplyMM(resultMatrix, 0, modelMatrix, 0, viewMatrix, 0);
    Matrix.multiplyMM(resultMatrix, 0, resultMatrix, 0, projectionMatrix, 0);
    

8、编码示例:

 private void calculateProjection(int windowWidth, int windowHeight) {
        float imageWidth = textureBitmap.getWidth();
        float imageHeight = textureBitmap.getHeight();

        float imageAspectRatio = imageWidth / imageHeight;
        float windowAspectRatio = (float) windowWidth / (float) windowHeight;

        // 正交投影参数
        float left, right, bottom, top;

        if (imageAspectRatio > windowAspectRatio) {
            // 图片相对窗口更宽
            float tb = (imageWidth / imageHeight) / (windowWidth / (float) windowHeight);
            top = tb;
            bottom = -tb;
            left = -1; // 可根据实际需求调整
            right = 1;  // 可根据实际需求调整
        } else {
            // 图片相对窗口更高或相等
            float lr = (windowWidth / (float) windowHeight) / (imageWidth / imageHeight);
            left = -lr;
            right = lr;
            top = 1; // 可根据实际需求调整
            bottom = -1; // 可根据实际需求调整
        }

        // 更新正交投影矩阵
        float[] projectionMatrix = new float[16];
        Matrix.orthoM(projectionMatrix, 0, left, right, bottom, top);
        square.setProjectionMatrix(projectionMatrix); // 这个square是你自定义的类来处理绘制
    }

关键点总结:

  • 计算正交投影矩阵
    • calculateProjection方法中,根据图片和窗口的宽高比计算并更新投影矩阵。
  • 根据条件设置矩阵的边界
    • 当图片宽高比大于窗口宽高比时,调整模型的高度。
    • 当图片宽高比小于或等于窗口宽高比时,调整模型的宽度。
  • 设置和渲染图像
    • 图片加载、绘制等逻辑将在Square类中进行。

五、编码实战:

我们需要对两种方式都进行编码验证。

1、视口变换:

  • 定义变量:

    文件路径:com/example/glsurfaceviewdemo/GLRenderTest.java

        private int mSurfaceWidth = 0; // 窗口宽
        private int mSurfaceHeight = 0; // 窗口高
        private int mViewportWidth = 0; // 视口宽
        private int mViewportHeight = 0; // 视口高
        private int mViewportX = 0; // 视口起始横坐标
        private int mViewportY = 0; // 视口起始纵坐标
    	private Bitmap mBitmap;
    

    注意,我顺便将图片操作挪到GLRenderTest当中了,之前是在com/example/glsurfaceviewdemo/TextureRender.java当中,通过构造函数传递给TextureRender即可。

  • 图片加载函数挪位置:

        public TextureRender(Context context, Bitmap bitmap) {
            mContext = context;
            mBitmap = bitmap;
            initialize();
        }
    

    对应GLRenderTest也需要修改:

        public GLRenderTest(Context context) {
            this.mContext = context;
            mBitmap = loadImage();
        }
        // 加载图片
        private Bitmap loadImage() {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inScaled = false;
            return BitmapFactory.decodeResource(mContext.getResources(), R.drawable.android_logo, options);
        }
        public void onDestroy() {
            // mTriangle.release();
            mBitmap.recycle(); // 回收bitmap
            mTextureRender.release();
        }
    
  • 添加视口变换函数:

    文件路径:com/example/glsurfaceviewdemo/GLRenderTest.java

        // 视口变换:通过调整视口大小,保证图片不被拉伸
        private void calculateViewport() {
            // 获取图片的宽高比
            float imageRatio = (float) mBitmap.getWidth() / mBitmap.getHeight();
            Log.e("Bitmap", "Bitmap111 width: " + mBitmap.getWidth() + ", height: " + mBitmap.getHeight());
            // 获取surface(窗口)的宽高比
            float surfaceRatio = (float) mSurfaceWidth / mSurfaceHeight;
    
            if (imageRatio > surfaceRatio) {
                // 图片宽高比大于窗口的宽高比,按照宽度填满
                mViewportWidth = mSurfaceWidth;
                mViewportHeight = (int) (mSurfaceWidth / imageRatio);
            } else {
                // 图片宽高比小于等于窗口的宽高比,按照高度填满
                mViewportWidth = (int) (mSurfaceHeight * imageRatio);
                mViewportHeight = mSurfaceHeight;
            }
    
            // 计算视口的中心位置
            mViewportX = (mSurfaceWidth - mViewportWidth) / 2;
            mViewportY = (mSurfaceHeight - mViewportHeight) / 2;
        }
    
  • 调用视口变换:

        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            mSurfaceWidth = width;
            mSurfaceHeight = height;
            calculateViewport(); // 计算视口参数
    
            GLES30.glViewport(mViewportX, mViewportY, mViewportWidth, mViewportHeight); // 设置视口
            // GLES30.glViewport(0, 0,  width, height); // 视口变换时候不能从屏幕左上角开始
        }
    
  • 完整代码:

    文件路径:com/example/glsurfaceviewdemo/GLRenderTest.java

    package com.example.glsurfaceviewdemo;
    
    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.opengl.GLES30;
    import android.opengl.GLSurfaceView;
    import android.opengl.Matrix;
    import android.util.Log;
    
    import javax.microedition.khronos.egl.EGLConfig;
    import javax.microedition.khronos.opengles.GL10;
    
    public class GLRenderTest implements GLSurfaceView.Renderer {
        private Triangle mTriangle;
        private TextureRender mTextureRender;
        private Context mContext;
        private int mSurfaceWidth = 0; // 窗口宽
        private int mSurfaceHeight = 0; // 窗口高
        private int mViewportWidth = 0; // 视口宽
        private int mViewportHeight = 0; // 视口高
        private int mViewportX = 0; // 视口起始横坐标
        private int mViewportY = 0; // 视口起始纵坐标
        private Bitmap mBitmap;
    
        public GLRenderTest(Context context) {
            this.mContext = context;
            mBitmap = loadImage();
        }
        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
            // mTriangle = new Triangle(mContext);
            mTextureRender = new TextureRender(mContext, mBitmap);
        }
    
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            mSurfaceWidth = width;
            mSurfaceHeight = height;
            calculateViewport(); // 计算视口参数
    
            GLES30.glViewport(mViewportX, mViewportY, mViewportWidth, mViewportHeight); // 设置视口
            // GLES30.glViewport(0, 0,  width, height); // 视口变换时候不能从屏幕左上角开始
        }
    
        @Override
        public void onDrawFrame(GL10 gl){
            GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
            // mTriangle.draw();
            mTextureRender.draw();
        }
    
        public void onDestroy() {
            // mTriangle.release();
            mBitmap.recycle(); // 回收bitmap
            mTextureRender.release();
        }
    
        // 加载图片
        private Bitmap loadImage() {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inScaled = false;
            return BitmapFactory.decodeResource(mContext.getResources(), R.drawable.android_logo, options);
        }
    
        // 视口变换:通过调整视口大小,保证图片不被拉伸
        private void calculateViewport() {
            // 获取图片的宽高比
            float imageRatio = (float) mBitmap.getWidth() / mBitmap.getHeight();
            // 获取surface(窗口)的宽高比
            float surfaceRatio = (float) mSurfaceWidth / mSurfaceHeight;
    
            if (imageRatio > surfaceRatio) {
                // 图片宽高比大于窗口的宽高比,按照宽度填满
                mViewportWidth = mSurfaceWidth;
                mViewportHeight = (int) (mSurfaceWidth / imageRatio);
            } else {
                // 图片宽高比小于等于窗口的宽高比,按照高度填满
                mViewportWidth = (int) (mSurfaceHeight * imageRatio);
                mViewportHeight = mSurfaceHeight;
            }
    
            // 计算视口的中心位置
            mViewportX = (mSurfaceWidth - mViewportWidth) / 2;
            mViewportY = (mSurfaceHeight - mViewportHeight) / 2;
        }
    }
    
    

    文件路径:com/example/glsurfaceviewdemo/TextureRender.java

    package com.example.glsurfaceviewdemo;
    
    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.opengl.GLES30;
    import android.opengl.GLUtils;
    import android.opengl.Matrix;
    import android.util.Log;
    
    import java.nio.ByteBuffer;
    import java.nio.ByteOrder;
    import java.nio.FloatBuffer;
    public class TextureRender {
        private Context mContext;
        private float[] mCoordData = {
                // 顶点坐标          纹理坐标
                -1.0f, 1.0f, 0.0f, 0.0f, 0.0f,  // 左上角
                -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, // 左下角
                1.0f, 1.0f, 0.0f, 1.0f, 0.0f,   // 右上角
                1.0f, -1.0f, 0.0f, 1.0f, 1.0f   // 右下角
        };
    
        private int mTextureId = -1;
        private FloatBuffer mCoordBuffer;
        private int mProgram = -1;
        private int mVboId;
        private float[] mTranslateMatrix = new float[16];
        private int mPositionHandle = -1; // 顶点位置属性的操作句柄
        private int mTexCoordHandle = -1; // 纹理坐标属性的操作句柄
        private int mTMatrixHandle = -1;  // 变换矩阵操作句柄,用于实现顶点的变换
        private int mSamplerHandle = -1;  // 纹理采样器操作句柄,相当于一个指向某个纹理单元的指针
        Bitmap mBitmap;
    
        public TextureRender(Context context, Bitmap bitmap) {
            mContext = context;
            mBitmap = bitmap;
            initialize();
        }
    
        private void initialize() {
            mTextureId = uploadTexture(); // 上传纹理到GPU
            initVertexBuffer();           // 初始化坐标数据
            initShaders(mContext);        // 加载并编译着色器
            initVbo();                    // 初始化 VBO
            initMatrix();                 // 初始化变换矩阵
            initHandles();                // 获取GPU和Shader的一些操作接口
        }
        // 初始化变换矩阵
        private void initMatrix() {
            Matrix.setIdentityM(mTranslateMatrix, 0); // 设置为单位矩阵
        }
        // 获取GPU和Shader的一些操作接口
        private void initHandles() {
            // 获取顶点坐标操作接口的句柄
            mPositionHandle = GLES30.glGetAttribLocation(mProgram, "aPosition");
            validateAttributeLocation(mPositionHandle, "aPosition");
            GLES30.glEnableVertexAttribArray(mPositionHandle); // 启用位置属性数组
            // 获取纹理坐标操作接口的句柄
            mTexCoordHandle = GLES30.glGetAttribLocation(mProgram, "aTexCoord");
            validateAttributeLocation(mTexCoordHandle, "aTexCoord");
            GLES30.glEnableVertexAttribArray(mTexCoordHandle); // 启用纹理坐标属性数组
            // 获取变换矩阵操作接口的句柄
            mTMatrixHandle = GLES30.glGetUniformLocation(mProgram, "uTMatrix");
            // 用于获取Shader当中纹理采样器的操作接口的句柄
            mSamplerHandle = GLES30.glGetUniformLocation(mProgram, "uSampler");
        }
        // 用于验证属性句柄的有效性
        private void validateAttributeLocation(int handle, String attributeName) {
            if (handle == -1) {
                Log.e("TextureRender", "Could not find attribute " + attributeName);
            }
        }
        // 绘制纹理
        public void draw() {
            // 激活着色器程序
            GLES30.glUseProgram(mProgram);
            // 绑定VBO
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);
            // 设置顶点坐标和纹理坐标给OpenGL
            setupVertexAttribPointer();
            // 上传变换矩阵
            GLES30.glUniformMatrix4fv(mTMatrixHandle, 1, false, mTranslateMatrix, 0);
            // 绑定纹理并设置采样器
            bindTexture();
            // 绘制矩形
            GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
            // 解绑当前的 VBO,避免后续操作意外影响到这个缓冲
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
            // 检查 OpenGL 错误
            checkOpenGLError();
        }
    
        // 设置顶点坐标和纹理坐标
        private void setupVertexAttribPointer() {
            // 指定位置属性的布局(设置顶点位置数据)
            // 3表示:每个顶点有3个浮点数(x, y, z),步长stride为 5*Float.BYTES,偏移量是0
            GLES30.glVertexAttribPointer(mPositionHandle, 3, GLES30.GL_FLOAT, false, 5 * Float.BYTES, 0);
            // 指定纹理坐标属性的布局(设置纹理坐标数据)
            // 2表示:每个纹理有 2 个浮点数(u, v),步长是同样是 5*Float.BYTES,偏移量是3*Float.BYTES(前三个是顶点坐标)
            GLES30.glVertexAttribPointer(mTexCoordHandle, 2, GLES30.GL_FLOAT, false, 5 * Float.BYTES, 3 * Float.BYTES);
        }
        // 绑定纹理
        private void bindTexture() {
            // 激活纹理单元 0
            GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
            // 绑定纹理
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId);
            // 上传纹理单元索引到Shader中的 uSampler 变量
            GLES30.glUniform1i(mSamplerHandle, 0);
        }
        // 检查OpengGL的错误
        private void checkOpenGLError() {
            int error = GLES30.glGetError();
            if (error != GLES30.GL_NO_ERROR) {
                Log.e("OpenGL", "OpenGL Error: " + error);
            }
        }
        // 加载并编译着色器
        private void initShaders(Context context) {
            String vertexShaderCode = ShaderController.loadShaderCodeFromFile("texture_vertex_shader.glsl", context);
            String fragmentShaderCode = ShaderController.loadShaderCodeFromFile("texture_fragment_shader.glsl", context);
            mProgram = ShaderController.createGLProgram(vertexShaderCode, fragmentShaderCode);
            if (mProgram == 0) {
                Log.e("TextureRender", "Failed to create OpenGL program.");
            }
        }
        // 初始化坐标数据
        private void initVertexBuffer() {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(mCoordData.length * 4);
            byteBuffer.order(ByteOrder.nativeOrder());
            mCoordBuffer = byteBuffer.asFloatBuffer();
            mCoordBuffer.put(mCoordData);
            mCoordBuffer.position(0);
        }
        // 初始化 VBO
        private void initVbo() {
            int[] vbos = new int[1];
            GLES30.glGenBuffers(1, vbos, 0);
            mVboId = vbos[0];
    
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);
            mCoordBuffer.position(0);
            GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, mCoordBuffer.capacity() * 4, mCoordBuffer, GLES30.GL_STATIC_DRAW);
        }
    
        // 上传纹理到GPU
        private int uploadTexture() {
            int[] textureIds = new int[1];
            GLES30.glGenTextures(1, textureIds, 0); // 创建纹理
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]); // 绑定纹理
            GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR); // 设置缩小策略
            GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR); // 设置放大策略
            GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, mBitmap, 0); // 纹理上传到GPU
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0); // 解绑纹理,避免后续误操作
            return textureIds[0];
        }
    
        public void release() {
            GLES30.glDeleteBuffers(1, new int[]{mVboId}, 0); // 删除 VBO
            GLES30.glDeleteProgram(mProgram); // 删除 shader program
        }
    }
    
    
    
  • 运行结果:

    在这里插入图片描述

    正常了✌!

2、正交投影:

  • 定义变量:

    文件路径:com/example/glsurfaceviewdemo/GLRenderTest.java

    	private float[] mProjectionMatrix = new float[16]; // 投影矩阵
        private float[] mViewMatrix = new float[16]; // 视图矩阵
        private float[] mMVPMatrix = new float[16]; // mvp矩阵
    
  • 计算视图矩阵和投影矩阵:

    文件路径:com/example/glsurfaceviewdemo/GLRenderTest.java

        // 正交投影变换
        private void calculateViewport2(int width, int height) {
            float imageAspectRatio = (float) mBitmap.getWidth() / (float) mBitmap.getHeight();
            float surfaceAspectRatio = (float) width / (float) height;
    
            if (imageAspectRatio > surfaceAspectRatio) {
                // 图片宽高比大于屏幕,按照宽度填满计算高度
                float tb = imageAspectRatio / surfaceAspectRatio;
                // 计算投影矩阵
                Matrix.orthoM(mProjectionMatrix, 0, -1.0f, 1.0f, -tb, tb, -1.0f, 1.0f);
            } else {
                // 图片宽高比小于等于屏幕,按照高度填满计算宽度
                float tb = surfaceAspectRatio / imageAspectRatio;
                // 计算投影矩阵
                Matrix.orthoM(mProjectionMatrix, 0, -tb, tb, -1.0f, 1.0f, -1.0f, 1.0f);
            }
            // 计算视图矩阵
            Matrix.setLookAtM(mViewMatrix, 0, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
            // 计算mvp矩阵
            Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
        }
    
  • 用MVP矩阵替换原来的单位矩阵:

    文件路径:com/example/glsurfaceviewdemo/TextureRender.java

    	private float[] mMVPMatrix; // mvp矩阵
    	// 接收mvp矩阵
        public void setCustomMVPMatrix(float[] mvpMatrix) {
            if (mvpMatrix.length == 16) { // 确保传入的数组长度为 16
                mMVPMatrix = new float[16];
                System.arraycopy(mvpMatrix, 0, mMVPMatrix, 0, 16);
            } else {
                Log.e("TextureRender", "mvp Matrix length invalid!");
            }
        }
    
  • 获取GPU上的MVP矩阵操作接口:

    文件路径:com/example/glsurfaceviewdemo/TextureRender.java

        private void initHandles() {
            // ... 省略非关键代码
            // 获取变换矩阵操作接口的句柄
            mMVPMatrixHandle = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix");
            // ... 省略非关键代码
        }
    

    同时修改点顶点着色器里面glsl代码:

    文件路径:texture_vertex_shader.glsl

    #version 300 es  // 指定 GLSL 版本
    precision mediump float;  // 定义浮点数精度
    
    // 统一变量
    uniform mat4 uMVPMatrix;  // 变换矩阵
    
    // 属性变量
    in vec4 aPosition;  // 顶点位置
    in vec2 aTexCoord;  // 纹理坐标
    
    // 输出变量
    out vec2 vTexCoord;  // 传递给片段着色器的纹理坐标
    
    void main() {
        // 应用变换矩阵并设置顶点位置
        gl_Position = uMVPMatrix * aPosition;
    
        // 将纹理坐标传递给片段着色器
        vTexCoord = aTexCoord;
    }
    

    这样,我们iu通过draw方法传递给了TextureRender,再通过mMVPMatrixHandle传递给了Shader,在Shader当中,通过uMVPMatrix变量接收这个矩阵。

    最后,在Shader的main函数中通过这个MVP矩阵乘以每个顶点数据,就对图片中的每个顶点完成了变换;

  • 完整代码:

    文件路径:com/example/glsurfaceviewdemo/GLRenderTest.java

    package com.example.glsurfaceviewdemo;
    
    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.opengl.GLES30;
    import android.opengl.GLSurfaceView;
    import android.opengl.Matrix;
    import android.util.Log;
    
    import javax.microedition.khronos.egl.EGLConfig;
    import javax.microedition.khronos.opengles.GL10;
    
    public class GLRenderTest implements GLSurfaceView.Renderer {
        private Triangle mTriangle;
        private TextureRender mTextureRender;
        private Context mContext;
        private int mSurfaceWidth = 0; // 窗口宽
        private int mSurfaceHeight = 0; // 窗口高
        private int mViewportWidth = 0; // 视口宽
        private int mViewportHeight = 0; // 视口高
        private int mViewportX = 0; // 视口起始横坐标
        private int mViewportY = 0; // 视口起始纵坐标
        private Bitmap mBitmap;
    
        private float[] mProjectionMatrix = new float[16]; // 投影矩阵
        private float[] mViewMatrix = new float[16]; // 视图矩阵
        private float[] mMVPMatrix = new float[16]; // mvp矩阵
    
        public GLRenderTest(Context context) {
            this.mContext = context;
            mBitmap = loadImage();
        }
        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
            // mTriangle = new Triangle(mContext);
            mTextureRender = new TextureRender(mContext, mBitmap);
        }
    
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            mSurfaceWidth = width;
            mSurfaceHeight = height;
    
            // calculateViewport(); // 计算视口参数
            calculateViewport2(width, height); // 计算正交投影参数
            mTextureRender.setCustomMVPMatrix(mMVPMatrix); // 设置mvp矩阵给渲染器
    
            // GLES30.glViewport(mViewportX, mViewportY, mViewportWidth, mViewportHeight); // 视口调整
            GLES30.glViewport(0, 0,  width, height); // 正交矩阵从屏幕左上角开始即可
        }
    
        @Override
        public void onDrawFrame(GL10 gl){
            GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
            // mTriangle.draw();
            mTextureRender.draw();
        }
    
        public void onDestroy() {
            // mTriangle.release();
            mBitmap.recycle(); // 回收bitmap
            mTextureRender.release();
        }
    
        // 加载图片
        private Bitmap loadImage() {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inScaled = false;
            return BitmapFactory.decodeResource(mContext.getResources(), R.drawable.android_logo, options);
        }
    
        // 视口变换:通过调整视口大小,保证图片不被拉伸
        private void calculateViewport() {
            // 获取图片的宽高比
            float imageRatio = (float) mBitmap.getWidth() / mBitmap.getHeight();
            // 获取surface(窗口)的宽高比
            float surfaceRatio = (float) mSurfaceWidth / mSurfaceHeight;
    
            if (imageRatio > surfaceRatio) {
                // 图片宽高比大于窗口的宽高比,按照宽度填满
                mViewportWidth = mSurfaceWidth;
                mViewportHeight = (int) (mSurfaceWidth / imageRatio);
            } else {
                // 图片宽高比小于等于窗口的宽高比,按照高度填满
                mViewportWidth = (int) (mSurfaceHeight * imageRatio);
                mViewportHeight = mSurfaceHeight;
            }
    
            // 计算视口的中心位置
            mViewportX = (mSurfaceWidth - mViewportWidth) / 2;
            mViewportY = (mSurfaceHeight - mViewportHeight) / 2;
        }
        // 正交投影变换
        private void calculateViewport2(int width, int height) {
            float imageAspectRatio = (float) mBitmap.getWidth() / (float) mBitmap.getHeight();
            float surfaceAspectRatio = (float) width / (float) height;
    
            if (imageAspectRatio > surfaceAspectRatio) {
                // 图片宽高比大于屏幕,按照宽度填满计算高度
                float tb = imageAspectRatio / surfaceAspectRatio;
                // 计算投影矩阵
                Matrix.orthoM(mProjectionMatrix, 0, -1.0f, 1.0f, -tb, tb, -1.0f, 1.0f);
            } else {
                // 图片宽高比小于等于屏幕,按照高度填满计算宽度
                float tb = surfaceAspectRatio / imageAspectRatio;
                // 计算投影矩阵
                Matrix.orthoM(mProjectionMatrix, 0, -tb, tb, -1.0f, 1.0f, -1.0f, 1.0f);
            }
            // 计算视图矩阵
            Matrix.setLookAtM(mViewMatrix, 0, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
            // 计算mvp矩阵
            Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
        }
    }
    
    

    文件路径:com/example/glsurfaceviewdemo/TextureRender.java

    package com.example.glsurfaceviewdemo;
    
    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.opengl.GLES30;
    import android.opengl.GLUtils;
    import android.opengl.Matrix;
    import android.util.Log;
    
    import java.nio.ByteBuffer;
    import java.nio.ByteOrder;
    import java.nio.FloatBuffer;
    public class TextureRender {
        private Context mContext;
        private float[] mCoordData = {
                // 顶点坐标          纹理坐标
                -1.0f, 1.0f, 0.0f, 0.0f, 0.0f,  // 左上角
                -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, // 左下角
                1.0f, 1.0f, 0.0f, 1.0f, 0.0f,   // 右上角
                1.0f, -1.0f, 0.0f, 1.0f, 1.0f   // 右下角
        };
    
        private int mTextureId = -1;
        private FloatBuffer mCoordBuffer;
        private int mProgram = -1;
        private int mVboId;
    
        private int mPositionHandle = -1; // 顶点位置属性的操作句柄
        private int mTexCoordHandle = -1; // 纹理坐标属性的操作句柄
        private int mMVPMatrixHandle = -1;  // 变换矩阵操作句柄,用于实现顶点的变换
        private int mSamplerHandle = -1;  // 纹理采样器操作句柄,相当于一个指向某个纹理单元的指针
        Bitmap mBitmap;
        private float[] mMVPMatrix; // mvp矩阵
    
        public TextureRender(Context context, Bitmap bitmap) {
            mContext = context;
            mBitmap = bitmap;
            initialize();
        }
    
        private void initialize() {
            mTextureId = uploadTexture(); // 上传纹理到GPU
            initVertexBuffer();           // 初始化坐标数据
            initShaders(mContext);        // 加载并编译着色器
            initVbo();                    // 初始化 VBO
            initHandles();                // 获取GPU和Shader的一些操作接口
        }
    
        // 获取GPU和Shader的一些操作接口
        private void initHandles() {
            // 获取顶点坐标操作接口的句柄
            mPositionHandle = GLES30.glGetAttribLocation(mProgram, "aPosition");
            validateAttributeLocation(mPositionHandle, "aPosition");
            GLES30.glEnableVertexAttribArray(mPositionHandle); // 启用位置属性数组
            // 获取纹理坐标操作接口的句柄
            mTexCoordHandle = GLES30.glGetAttribLocation(mProgram, "aTexCoord");
            validateAttributeLocation(mTexCoordHandle, "aTexCoord");
            GLES30.glEnableVertexAttribArray(mTexCoordHandle); // 启用纹理坐标属性数组
            // 获取变换矩阵操作接口的句柄
            mMVPMatrixHandle = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix");
            // 用于获取Shader当中纹理采样器的操作接口的句柄
            mSamplerHandle = GLES30.glGetUniformLocation(mProgram, "uSampler");
        }
        // 用于验证属性句柄的有效性
        private void validateAttributeLocation(int handle, String attributeName) {
            if (handle == -1) {
                Log.e("TextureRender", "Could not find attribute " + attributeName);
            }
        }
        // 绘制纹理
        public void draw() {
            // 激活着色器程序
            GLES30.glUseProgram(mProgram);
            // 绑定VBO
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);
            // 设置顶点坐标和纹理坐标给OpenGL
            setupVertexAttribPointer();
            // 上传变换矩阵
            GLES30.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
            // 绑定纹理并设置采样器
            bindTexture();
            // 绘制矩形
            GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
            // 解绑当前的 VBO,避免后续操作意外影响到这个缓冲
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
            // 检查 OpenGL 错误
            checkOpenGLError();
        }
    
        // 接收mvp矩阵
        public void setCustomMVPMatrix(float[] mvpMatrix) {
            if (mvpMatrix.length == 16) { // 确保传入的数组长度为 16
                mMVPMatrix = new float[16];
                System.arraycopy(mvpMatrix, 0, mMVPMatrix, 0, 16);
            } else {
                Log.e("TextureRender", "mvp Matrix length invalid!");
            }
        }
    
        // 设置顶点坐标和纹理坐标
        private void setupVertexAttribPointer() {
            // 指定位置属性的布局(设置顶点位置数据)
            // 3表示:每个顶点有3个浮点数(x, y, z),步长stride为 5*Float.BYTES,偏移量是0
            GLES30.glVertexAttribPointer(mPositionHandle, 3, GLES30.GL_FLOAT, false, 5 * Float.BYTES, 0);
            // 指定纹理坐标属性的布局(设置纹理坐标数据)
            // 2表示:每个纹理有 2 个浮点数(u, v),步长是同样是 5*Float.BYTES,偏移量是3*Float.BYTES(前三个是顶点坐标)
            GLES30.glVertexAttribPointer(mTexCoordHandle, 2, GLES30.GL_FLOAT, false, 5 * Float.BYTES, 3 * Float.BYTES);
        }
        // 绑定纹理
        private void bindTexture() {
            // 激活纹理单元 0
            GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
            // 绑定纹理
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId);
            // 上传纹理单元索引到Shader中的 uSampler 变量
            GLES30.glUniform1i(mSamplerHandle, 0);
        }
        // 检查OpengGL的错误
        private void checkOpenGLError() {
            int error = GLES30.glGetError();
            if (error != GLES30.GL_NO_ERROR) {
                Log.e("OpenGL", "OpenGL Error: " + error);
            }
        }
        // 加载并编译着色器
        private void initShaders(Context context) {
            String vertexShaderCode = ShaderController.loadShaderCodeFromFile("texture_vertex_shader.glsl", context);
            String fragmentShaderCode = ShaderController.loadShaderCodeFromFile("texture_fragment_shader.glsl", context);
            mProgram = ShaderController.createGLProgram(vertexShaderCode, fragmentShaderCode);
            if (mProgram == 0) {
                Log.e("TextureRender", "Failed to create OpenGL program.");
            }
        }
        // 初始化坐标数据
        private void initVertexBuffer() {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(mCoordData.length * 4);
            byteBuffer.order(ByteOrder.nativeOrder());
            mCoordBuffer = byteBuffer.asFloatBuffer();
            mCoordBuffer.put(mCoordData);
            mCoordBuffer.position(0);
        }
        // 初始化 VBO
        private void initVbo() {
            int[] vbos = new int[1];
            GLES30.glGenBuffers(1, vbos, 0);
            mVboId = vbos[0];
    
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);
            mCoordBuffer.position(0);
            GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, mCoordBuffer.capacity() * 4, mCoordBuffer, GLES30.GL_STATIC_DRAW);
        }
    
        // 上传纹理到GPU
        private int uploadTexture() {
            int[] textureIds = new int[1];
            GLES30.glGenTextures(1, textureIds, 0); // 创建纹理
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]); // 绑定纹理
            GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR); // 设置缩小策略
            GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR); // 设置放大策略
            GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, mBitmap, 0); // 纹理上传到GPU
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0); // 解绑纹理,避免后续误操作
            return textureIds[0];
        }
    
        public void release() {
            GLES30.glDeleteBuffers(1, new int[]{mVboId}, 0); // 删除 VBO
            GLES30.glDeleteProgram(mProgram); // 删除 shader program
        }
    }
    

    文件路径:texture_vertex_shader.glsl

    #version 300 es  // 指定 GLSL 版本
    precision mediump float;  // 定义浮点数精度
    
    // 统一变量
    uniform mat4 uMVPMatrix;  // 变换矩阵
    
    // 属性变量
    in vec4 aPosition;  // 顶点位置
    in vec2 aTexCoord;  // 纹理坐标
    
    // 输出变量
    out vec2 vTexCoord;  // 传递给片段着色器的纹理坐标
    
    void main() {
        // 应用变换矩阵并设置顶点位置
        gl_Position = uMVPMatrix * aPosition;
    
        // 将纹理坐标传递给片段着色器
        vTexCoord = aTexCoord;
    }
    
  • 运行结果:

    在这里插入图片描述

    也成功了!

六、总结:

本文主要介绍了两种防止拉伸的方法,一个是通过修改视口来防止拉伸,第二个是通过设置投影来完成,设置投影主要是设置MVP矩阵。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2258509.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【实现多网卡电脑的网络连接共享】

电脑A配备有两张网卡,分别命名为eth0和eth1(对于拥有超过两张网卡的情况,解决方案相似)。其中,eth0网卡能够连接到Internet,而eth1网卡则通过网线直接与另一台电脑B相连(在实际应用中&#xff0…

聊聊在应用层面实现内网穿透功能是否可行

前言 最近接手了供方开发的网关项目,交接文档里面有个内网穿透的功能,一下子就吸引的我的目光。实现这个内网穿透的背景是业务部门有些业务是部署在公网,这些公网的业务想访问内网的业务,但因为公网和内网没打通,导致…

头歌 计算机操作系统 Linux之线程同步二

第1关:信号量 任务描述 在上一个实训中,我们学习了使用互斥锁来实现线程的同步,Linux系统中还提供了另一个类似互斥锁的线程不同操作,那就是信号量。 本关任务:学会使用信号量来实现线程间的同步与互斥。 相关知识 …

基于MinIO打造高可靠分布式“本地”文件系统

MinIO是一款高性能的对象存储服务,而S3协议是由亚马逊Web服务(AWS)制定的一种标准协议,用于云存储服务之间的数据交换。MinIO与S3协议的关系在于,MinIO实现了S3协议的接口,这意味着用户可以使用与AWS S3相同…

【MIT-OS6.S081作业1.3】Lab1-utilities primes

本文记录MIT-OS6.S081 Lab1 utilities 的primes函数的实现过程 文章目录 1. 作业要求primes (moderate)/(hard) 2. 实现过程2.1 代码实现 1. 作业要求 primes (moderate)/(hard) Write a concurrent version of prime sieve using pipes. This idea is due to Doug McIlroy, in…

Js如和返回数组中的指定列

一、需求 日常工作中需要返回数组中的指定列,例如Echarts 和 下拉框 选择 id,value 类似这种都需要在数组中提取指定列元素。 二、代码示例 const products [{ name: "商品1", price: 100, inventory: 50 },{ name: "商品2", pri…

C++的一些经典算法

以下是C的一些经典算法: 一、排序算法 冒泡排序(Bubble Sort) 原理: 它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换…

35.1 thanos项目介绍和二进制部署

本节重点介绍 : 核心优点 无需维护存储,存储高可用: 利用廉价的公有云对象存储,高可用长时间存储,数据降采样:利用Compactor降采样完全适配原生prometheus查询接口:Query实现多级数据缓存配置 二进制部署 …

【PlantUML系列】状态图(六)

一、状态图的组成部分 状态:对象在其生命周期内可能处于的条件或情形,使用 state "State Name" as Statename 表示。初始状态:表示对象生命周期的开始,使用 [*] 表示。最终状态:表示对象生命周期的结束&…

Android 15(V)新功能适配,雕琢移动细节之美

Android 15,内部代号为Vanilla Ice Cream,是Android移动操作系统的最新主要版本,于2024年2月16日在开发者预览版1中发布。Android 15源代码于 2024年9月4日发布。Android 15稳定版于2024年10月15日发布。 以下是针对 Android 15(…

【零成本抽象】基本概念与在C++中的实现

零成本抽象概念是由 Bjarne Stroustrup 提出的,他在 1994 年的著作中就有相关设想,2016 年其在 C++ 大会登台演讲时,明确阐述了 C++ 中的 “零成本抽象” 这一理念。 一、零成本抽象概念 Bjarne Stroustrup提出的零成本抽象概念,是指在编程中使用高级抽象机制时,不会产生…

android编译assets集成某文件太大更新导致git仓库变大

不知道大家有没有类似的困扰,你的工程assets文件过大,我曾经在某度车机地图团队工作过一段时间时候,每次发包会集成一个上百MB的文件。工作一段时间你的git仓库将会增加特别多。最后,你会发现你如果重新git clone这个仓库会非常大…

F5-TTS文本语音合成模型的使用和接口封装

F5-TTS文本语音生成模型 1. F5-TTS的简介 2024年10月8日,上海交通大学团队发布,F5-TTS (A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching) 是一款基于扩散Transformer和ConvNeXt V2的文本转语音 (TTS) 模型。F5-TTS旨在生成流…

克隆选择算法复现

克隆选择算法复现 基于克隆选择算法求解0 - 1背包问题的代码复现文档一、背景和意义(一)背景(二)意义 二、算法原理(一)克隆选择算法基础(二)受体编辑机制 三、算法流程(…

Scala的隐式对象

Scala中,隐式对象(implicit object)是一种特殊的对象,它可以使得其成员(如方法和值)在特定的上下文中自动可用,而无需显式地传递它们。隐式对象通常与隐式参数和隐式转换一起使用,以…

观察者模式的理解和实践

引言 在软件开发中,设计模式是开发者们为了解决常见的设计问题而总结出来的一系列最佳实践。观察者模式(Observer Pattern)是其中一种非常经典且使用率极高的设计模式。它主要用于定义对象之间的一对多关系,使得当一个对象的状态发…

windows下Qt5自动编译配置QtMqtt环境(11)

文章目录 [toc]1、概述2、准备1.1 下载源码1.2 配置环境1.3 解释原理 3、编译4、验证5、参考6、视频 更多精彩内容👉内容导航 👈👉Qt网络编程 👈 1、概述 Qt默认是不包含mqtt库的,如果需要使用到mqtt库就只能自己编译配…

【6】数据分析检测(DataFrame 1)

学习目标3 昨天,我们学习了Series。 而Pandas的另一种数据类型:DataFrame,在许多特性上和Series有相似之处。 今天,我们将学习DataFrame的相关知识: 1. DataFrame的概念 2. 构造一个DataFrame 3. DataFrame的常用…

如何选择安全、可验证的技术?

澳大利亚信号局的澳大利亚网络安全中心 (ASD 的 ACSC) 发布了一份指导文件,题为《选择安全和可验证的技术》,旨在帮助组织在采购软件(专有或开源)、硬件(例如物联网设备)和云服务(SaaS、MSP 服务…

趣味编程:猜拳小游戏

1.简介 这个系列的第一篇以猜拳小游戏开始,这是源于我们生活的灵感,在忙碌的时代中,我们每个人都在为自己的生活各自忙碌着,奔赴着自己所走向的那条路上,即使遍体鳞伤。 但是,生活虽然很苦,也不…