Android OpenGLES2.0开发(三):绘制一个三角形

news2024/11/18 16:24:20

我们总是对陌生人太客气,而对亲密的人太苛刻

上一篇文章中,我们已经将OpenGL ES环境搭建完成。接下来我们就可以开始我们的绘图之旅了。该篇我们讲解最基本图形三角形的绘制,这是一切绘制的基础。在OpenGL ES的世界里一切图形都可以由三角形拼接绘制而成。

在Android官方文档中也介绍了三角形的绘制,本文案例我们参照了官方文档,但也做了进一步的改进,希望你能通过本节对OpenGL ES绘制图形有一个初步的认识。

坐标系

在绘制图形前我们要了解OpenGL ES的坐标系,我们知道Android的坐标系左上角为原点[0,0],而OpenGL ES的坐标系如下图:
请添加图片描述

OpenGL ES的坐标系中原点[0,0]在屏幕的中心,无论屏幕是正方形还是长方形,四个点的坐标都如上图所示,也就是边长都是为2的正方形

聪明的同学可能已经有疑问了,OpenGL ES坐标系明显是按照屏幕是正方形来的,那么如果屏幕为长方形,我按照比例绘制图形肯定会变形的。这里给出肯定的回答:是的!至于怎么解决先放下。

接下来我们先画一个三角形,至于遇到的问题,我们一个个解决^_^

三角形绘制

  1. 设置要绘制图形的坐标和颜色数据
  2. 定义顶点着色器片段着色器
  3. 创建Shader程序并链接编译好
  4. 绘制图形:使用Shader程序,将顶点、颜色数据传递到显存

1. 顶点颜色数据定义

在OpenGL ES中绘制图形必须先定义好坐标,确定图形的位置才能进行绘制。我们为坐标定义浮点数的顶点数组,然后我在构造方法中将浮点数组转化为ByteBuffer,后续我们会将它传递到OpenGL ES图像管道进行处理。

public class Triangle {

    // 顶点坐标缓冲区
    private FloatBuffer vertexBuffer;

    // 此数组中每个顶点的坐标数
    static final int COORDS_PER_VERTEX = 3;
    // 三角形三个点的坐标,逆时针绘制
    static float triangleCoords[] = {   // 坐标逆时针顺序
            0.0f, 0.616f, 0.0f, // top
            -0.5f, -0.25f, 0.0f, // bottom left
            0.5f, -0.25f, 0.0f  // bottom right
    };

    // 设置颜色为白色
    float color[] = {1.0f, 1.0f, 1.0f, 1.0f};

    public Triangle() {
        // 初始化形状坐标的顶点字节缓冲区
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (number of coordinate values * 4 bytes per float)
                triangleCoords.length * 4);
        // use the device hardware's native byte order
        bb.order(ByteOrder.nativeOrder());

        // create a floating point buffer from the ByteBuffer
        vertexBuffer = bb.asFloatBuffer();
        // add the coordinates to the FloatBuffer
        vertexBuffer.put(triangleCoords);
        // set the buffer to read the first coordinate
        vertexBuffer.position(0);
    }
}

根据代码中定义的三角形的坐标,我们大概画出的三角形如下图,应该是一个等边三角形
在这里插入图片描述

形状面和环绕

在 OpenGL 中,形状的面是由三维或更多点定义的三维表面 空间。一组三个或更多个三维点(在 OpenGL 中称为顶点)具有一个正面以及一个背面。如何知道哪一面为正面,哪一面为背面呢?这个问题问得好! 答案与环绕或者定义形状点的方向有关

请添加图片描述
在此示例中,三角形的点按如上顺序定义,这也决定了他们是按逆时针的方向绘制,绘制这些坐标的顺序定义了形状的环绕方向。默认情况下,在OpenGL中,逆时针绘制的面是正面,而另外一面是背面

为什么重要的是要知道形状的哪个面是正面?答案与OpenGL的常用功能有关,称为面部剔除。面部剔除是OpenGL环境的一个选项,它允许渲染管道忽略(不计算或绘制)形状的背面,从而节省时间,内存和处理周期:

// enable face culling feature
gl.glEnable(GL10.GL_CULL_FACE);
// specify which faces to not draw
gl.glCullFace(GL10.GL_BACK);

请务必按照逆时针绘制顺序定义 OpenGL 形状的坐标

2. 创建着色器代码

public class Triangle {

    // 顶点着色器代码
    private final String vertexShaderCode =
            "attribute vec4 vPosition;\n" +
                    "void main() {\n" +
                    "  gl_Position = vPosition;\n" +
                    "}\n";

    // 片段着色器代码
    private final String fragmentShaderCode =
            "precision mediump float;\n" +
                    "uniform vec4 vColor;\n" +
                    "void main() {\n" +
                    "  gl_FragColor = vColor;\n" +
                    "}\n";

    ...
}
变量名说明备注
vPosition顶点坐标数据,我们第一步定义的顶点数据需要传给这个变量该变量名可随意修改
gl_PositionShader的内置变量,就是图形的顶点位置该变量名不可修改
vColor图元(像素)的颜色,我们第一步定义的颜色需要传给这个变量该变量名可随意修改
gl_FragColorShader的内置变量,图元颜色该变量名不可修改

上面先简单介绍下变量的含义,后续我们会详细讲解GLSL语言。

3. 创建Shader程序并链接

我们需要对上面的着色器语言进行编译链接后,才能在OpenGL ES环境中使用。编译此代码,我们需要一个实用的方法:

public class GLESUtils {

    /**
     * 加载着色器代码
     *
     * @param type
     * @param shaderCode
     * @return
     */
    public static int loadShader(int type, String shaderCode) {

        // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
        // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
        int shader = GLES20.glCreateShader(type);

        // add the source code to the shader and compile it
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);

        return shader;
    }
}

为了匹配Renderer生命周期方法onSurfaceCreated,我们在Triangle中创建surfaceCreated方法用来编译链接Shader程序,创建surfaceChanged来更新OpenGL ES画布大小

public class Triangle() {
    ...

    /**
     * OpenGL ES程序句柄
     */
    private int mProgram;

    public void surfaceCreated() {
        // 加载顶点着色器代码
        int vertexShader = GLESUtils.loadShader(GLES20.GL_VERTEX_SHADER,
                vertexShaderCode);
        // 加载片段着色器代码
        int fragmentShader = GLESUtils.loadShader(GLES20.GL_FRAGMENT_SHADER,
                fragmentShaderCode);

        // 创建空的OpenGL ES程序
        mProgram = GLES20.glCreateProgram();
        // 将顶点着色器添加到程序中
        GLES20.glAttachShader(mProgram, vertexShader);
        // 将片段着色器添加到程序中
        GLES20.glAttachShader(mProgram, fragmentShader);
        // 链接OpenGL ES程序
        GLES20.glLinkProgram(mProgram);
    }

    public void surfaceChanged(int width, int height) {
        // 设置OpenGL ES画布大小
        GLES20.glViewport(0, 0, width, height);
    }
}

4. 绘制图形

此时我们已经定义好了三角形的顶点坐标数据、片元颜色值、OpenGL ES着色器程序。接下来我们只需要将坐标数据、片元颜色值传递给着色器程序,然后执行绘制我们就可以将三角形画出来了。我们定义一个单独的方法draw用来执行绘制三角形

4.1 将Shader程序添加到OpenGL ES环境

public class Triangle() {
    public void draw() {
        // 将程序添加到OpenGL ES环境
        GLES20.glUseProgram(mProgram);

        // 重新绘制背景色为黑色
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
		...
    }
}

4.2 传递顶点坐标和片元颜色数据

我们要从java内存将数据传递给OpenGL ES显存环境中,需要获取Shader程序属性的句柄值

注意:获取属性句柄要和Shader程序中定义的属性变量名字一样。获取属性句柄后,我们操作属性句柄就可以将数据传递给对应的变量了。

public class Triangle() {

	...
    /**
     * Shader程序中顶点属性的句柄
     */
    private int positionHandle;
    /**
     * Shader程序中颜色属性的句柄
     */
    private int colorHandle;
    
    public void surfaceCreated() {
        // 加载Shader程序代码
        
		...
        // 获取顶点着色器vPosition成员的句柄
        positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        // 获取片段着色器vColor成员的句柄
        colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
    }
}

draw方法中设置顶点数据和颜色值

public class Triangle() {

	...
    private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
    private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
	
    public void draw() {
        // 将程序添加到OpenGL ES环境
		...
		
        // 为三角形顶点启用控制柄
        GLES20.glEnableVertexAttribArray(positionHandle);
        // 准备三角坐标数据
        GLES20.glVertexAttribPointer(
                positionHandle,     // 执行要配置的属性句柄(编号)
                COORDS_PER_VERTEX,  // 指定每个顶点属性的分量数
                GLES20.GL_FLOAT,    // 指定每个分量的数据类型
                false,              // 指定是否将数据归一化到 [0,1] 或 [-1,1] 范围内
                vertexStride,       // (步长)指定连续两个顶点属性间的字节数。如果为 0,则表示顶点属性是紧密排列的
                vertexBuffer        // 指向数据缓冲对象
        );

        // 设置绘制三角形的颜色
        GLES20.glUniform4fv(colorHandle, 1, color, 0);

        // 画三角形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

        // 禁用顶点阵列
        GLES20.glDisableVertexAttribArray(positionHandle);
    }
}

4.3 在GLSurfaceView中绘制三角形

以上我们已经将OpenGL ES绘制三角形的流程全部讲完,接下来我们只需要在GLSurfaceView中创建Triangle类并执行对应的方法

我们在上一篇中Android OpenGLES2.0开发(二):环境搭建已经搭建好了OpenGL ES环境,现在只需在Renderer接口中添加Triangle即可

public class TriangleGLSurfaceView extends GLSurfaceView {

    private Context mContext;
    private MyRenderer mMyRenderer;

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

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

    private void init(Context context) {
        mContext = context;
        mMyRenderer = new MyRenderer();
        setEGLContextClientVersion(2);
        setRenderer(mMyRenderer);
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }

    static class MyRenderer implements Renderer {

        Triangle mTriangle;

        public MyRenderer() {
            mTriangle = new Triangle();
        }

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            mTriangle.surfaceCreated();
        }

        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            mTriangle.surfaceChanged(width, height);
        }

        @Override
        public void onDrawFrame(GL10 gl) {
            mTriangle.draw();
        }
    }
}

将GLSurfaceView添加到布局中,运行程序我们可以看到绘制的效果如下图所示:
请添加图片描述
经过上面坐标系的讲解,我们应该能料想到绘制的结果并不是一个正三角形,至于原因我想大家也应该知道,至于怎么解决我们后续再讲

最后

希望你根据上面的步骤一步一步将代码敲出来,相信你肯定对OpenGL ES绘制有一个全面的了解。虽然只是绘制一个三角形,但是上面的代码基本上是后续一切绘制的基础,也算是一个模板代码,后续别的绘制基本上就是对上面的代码微调即可实现。

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

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

相关文章

Linux云计算 |【第四阶段】RDBMS1-DAY5

主要内容: 试图概述(创建视图VIEW、修改、查看、删除)、变量(全局变量、会话变量、用户变量、局部变量)、存储过程(创建、调用、删除存储过程)、流程控制结构(分支结构:…

必备!8款热门网页制作工具大汇总

在过去,网站的构建主要依赖专业人员手动编写HTML、CSS和JavaScript等代码。然而,如今涌现出越来越多智能化的网页制作工具,使得任何人都能在零编码基础上轻松创建和设计网站。本文将向您介绍2022年热门的网页制作工具。选择合适的网页制作工具…

【WPF】桌面程序开发之窗口的用户控件详解

使用Visual Studio开发工具,我们可以编写在Windows系统上运行的桌面应用程序。其中,WPF(Windows Presentation Foundation)项目是一种常见的选择。然而,对于初学者来说,WPF项目中xaml页面的布局设计可能是一…

Naive UI 选择器 Select 的:render-label 怎么使用(Vue3 + TS)

项目场景: 在Naive UI 的 选择器 Select组件中 ,如何实现下面的效果 ,在下拉列表中,左边展示色块,右边展示文字。 Naive UI 的官网中提到过这个实现方法,有一个render-label的api,即&#xff…

Golang | Leetcode Golang题解之第442题数组中重复的数据

题目&#xff1a; 题解&#xff1a; func findDuplicates(nums []int) (ans []int) {for _, x : range nums {if x < 0 {x -x}if nums[x-1] > 0 {nums[x-1] - nums[x-1]} else {ans append(ans, x)}}return }

有通话质量更好的蓝牙耳机推荐吗?高品质的平价开放式耳机推荐

个人认为开放式耳机在通话方面还是表现不错的&#xff0c;主要有以下几个原因&#xff1a; 首先&#xff0c;在麦克风设计与配置方面&#xff1a; 拥有高品质麦克风硬件。优质的开放式耳机往往会配备高性能的麦克风&#xff0c;这些麦克风灵敏度较高&#xff0c;能够精准地捕捉…

1.2.1 HuggingFists安装说明-Linux安装

Linux版安装说明 下载地址 【GitHub】https://github.com/Datayoo/HuggingFists 【百度网盘】https://pan.baidu.com/s/12-qzxARjzRjYFvF8ddUJQQ?pwd2024 安装说明 环境要求 操作系统&#xff1a;CentOS7 硬件环境&#xff1a;至少4核8G&#xff0c;系统使用Containerd…

如何理解矩阵的复数特征值和特征向量?

实数特征值的直观含义非常好理解&#xff0c;它就是在对应的特征向量方向上的纯拉伸/压缩。 而复数特征值&#xff0c;我们可以把它放在复数域中理解。但是这里给出一个不那么简洁、但是更加直观的理解方式&#xff1a;把它放在实空间中。那么复数特征值表现的就是旋转等比放大…

Linux进程间的通信(三)IPC-信号通信和system-V消息队列

目录 信号通信 信号动作的改写 测试 信号的发送 消息队列 消息队列创建要用到的函数 send.c&#xff1a; recv.c 控制消息队列 信号通信 信号通信是一种在 Unix 和类 Unix 系统&#xff08;如 Linux&#xff09;中用于进程间异步通知的机制。信号是一种软件中断&#x…

数据库软题3-专门的集合运算

一、投影&#xff08;筛选列&#xff09; 题1 题2 二、选择(筛选行) 三、连接 3.自然连接 题1-自然连接的属性列数&#xff08;几元关系&#xff09;和元组数 解析&#xff1a; 题2-自然连接的属性列数&#xff08;几元关系&#xff09;和元组数 自然连接后的属性个数 A列…

SpringBoot3+Druid YAML配置

背景 Druid连接池是阿里巴巴开源的数据库连接池项目。Druid连接池为监控而生&#xff0c;内置强大的监控功能&#xff0c;监控特性不影响性能。功能强大&#xff0c;能防SQL注入&#xff0c;内置Loging能诊断Hack应用行为。现在已经SpringBoot3&#xff0c;Druid的配置也需要随…

java基础(4)类和对象

目录 1.前言 2.正文 2.1类的定义与使用 2.1.1类的定义 2.1.2类的实例化 2.1.3this引用 2.1.3.1 访问当前对象的成员变量 2.1.3.2调用当前对象的成员方法 2.1.3.3构造函数中的 this 2.1.3.4归纳this 2.2封装 2.2.1封装的定义 2.2.2访问修饰符 2.3static 2.3.1sta…

靠谱的建站公司怎么找?2024高端定制开发建站公司推荐

和所有行业一样&#xff0c;网站建设行业内部现在处于一个鱼龙混杂的状态&#xff0c;大多数的网建企业所做的是与模板网站有关的业务&#xff0c;少部分企业专精于定制高端网站。在低端市场逐渐饱和后&#xff0c;无论什么企业都会有开始进行产品升级的需求&#xff0c;而高端…

刚面试完的前端面试题

今天晚上参加了一场长达40多分钟的技术面。我觉得面试官非常专业&#xff0c;问的问题也都是很棒的&#xff01;自己很多知识都需要学习。所以我决定回想并记录下来。回答不对的地方欢迎大家指正&#xff01; 我自己在小本本上回忆出来的大概就是26道题。后期我会持续更新我学习…

(undone) 阅读 MapReduce 论文笔记

参考&#xff1a;https://pdos.csail.mit.edu/6.824/papers/mapreduce.pdf 摘要&#xff1a;简单介绍了 MapReduce 是在大型分布式系统上工作的 Introduction 的内容总结&#xff1a; 1.介绍背景&#xff1a;为什么我们需要分布式系统&#xff1f;MapReduce 的意义是哪些 2.简…

2024icpc(Ⅱ)网络赛补题E

E. Escape 思路&#xff1a; 可以看成 Sneaker 和杀戮机器人都不能在原地停留&#xff0c;然后杀戮机器人有个活动范围限制。如果 Sneaker 和杀戮机器人可以在原地停留&#xff0c;那么 Sneaker 到达一个点肯定会尽可能早&#xff0c;而且时间必须比杀戮机器人到达这个点短。那…

从传统 RAG 到图 RAG,赋予大型语言模型更强大的知识力量

大型语言模型 (LLMs) 在固定数据集上进行训练&#xff0c;其知识在最后一次训练更新时就已固定。 ChatGPT 的常规用户可能已经注意到其众所周知的局限性&#xff1a;“训练数据截止到 2021 年 9 月”。 这种局限性会导致模型产生不准确或过时的响应&#xff0c;因为它们会“幻…

【计算机网络】初识Socket编程,揭秘Socket编程艺术--UDP篇

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;Linux从入门到进阶 欢迎大家点赞收藏评论&#x1f60a; 目录 Socket编程准备知识理解源IP地址和目的IP地址 认识端口号 网络字节序 socket编程socket编程接口socket系统调用bzero函数struct soc…

生信机器学习入门4 - 构建决策树(Decision Tree)和随机森林(Random Forest)分类器

机器学习文章回顾 生信机器学习入门1 - 数据预处理与线性回归&#xff08;Linear regression&#xff09;预测 生信机器学习入门2 - 机器学习基本概念 生信机器学习入门3 - Scikit-Learn训练机器学习分类感知器 生信机器学习入门4 - scikit-learn训练逻辑回归&#xff08;L…

【Android 14源码分析】Activity启动流程-2

忽然有一天&#xff0c;我想要做一件事&#xff1a;去代码中去验证那些曾经被“灌输”的理论。                                                                                  – 服装…