OpenGL精简案例一

news2024/11/26 4:49:00

文章目录

  • 案例一 绘制点线面
    • 定义Renderer
    • 顶点着色器
    • 片段着色器
      • 内置的特殊变量
    • 应用场景
    • 工具ShaderHelper
    • 工具 TextResourceReader
    • 效果图如下
  • 结论

案例一 绘制点线面

定义Renderer


import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.util.Log;

import com.guide.opengllib.R;
import com.guide.opengllib.simple.ShaderHelper;
import com.guide.opengllib.simple.TextResourceReader;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import static android.opengl.GLES20.GL_COLOR_BUFFER_BIT;
import static android.opengl.GLES20.GL_FLOAT;
import static android.opengl.GLES20.GL_LINES;
import static android.opengl.GLES20.GL_POINTS;
import static android.opengl.GLES20.GL_TRIANGLE_FAN;
import static android.opengl.GLES20.glClear;
import static android.opengl.GLES20.glClearColor;
import static android.opengl.GLES20.glDrawArrays;
import static android.opengl.GLES20.glEnableVertexAttribArray;
import static android.opengl.GLES20.glGetAttribLocation;
import static android.opengl.GLES20.glVertexAttribPointer;
import static android.opengl.GLES20.glViewport;

/**
 */
public class CRenderer implements GLSurfaceView.Renderer {

    private String TAG = "Qm";
    //缓冲区
    private final FloatBuffer vertexData;
    //上下文
    private Context context;
    //float buffer大小
    private static final int BYTES_PER_FLOAT = 4;
    //程序
    private int program;
    //颜色着色器引用
    private int aColorLocation;
    //顶点着色器引用
    private int aPositionLocation;
    //glsl 颜色着色器坐标
    private static final String A_COLOR = "a_Color";
    //glsl 顶点着色器坐标
    private static final String A_POSITION = "a_Position";
    //顶点着色器 每行点个数
    private static final int POSITION_COMOPNENT_COUNT = 2;
    //颜色着色器 每行颜色个数
    private static final int COLOR_COMPONENT_CONT = 3;
    //每行间隔
    private static final int STRIDE = (POSITION_COMOPNENT_COUNT + COLOR_COMPONENT_CONT) *
            BYTES_PER_FLOAT;

    /**
     * 1.添加坐标点; OpenGl中,只能绘制点,直线和三角形
     * 在定义三角形时,我们总是以逆时针的顺序排列顶点,这称为卷曲顺序。
     */
    float[] tableVertices = {
            // Order of coordinates: X, Y, R, G, B

            // Triangle Fan
            0f, 0f, 1f, 1f, 1f,
            -0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
            0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
            0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
            -0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
            -0.5f, -0.5f, 0.0f, 1.0f, 0.0f,

            // Line 1
            -0.5f, 0f, 1f, 0f, 0f,
            0.5f, 0f, 1f, 0f, 0f,

            // Mallets
            0f, -0.25f, 0f, 0f, 1f,
            0f, 0.25f, 1f, 0f, 0f
    };

    public CRenderer(Context context) {
        this.context = context;
        //ByteBuffer.allocateDirect 分配一块内存;参数是分配多少字节的内存块。
        //order 季节缓冲区按照本地字节序组织它的内容。
        vertexData = ByteBuffer.allocateDirect(tableVertices.length * BYTES_PER_FLOAT)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        vertexData.put(tableVertices);
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.w(TAG, "onSurfaceCreated");
        //设置背景清除颜色为红色。
        //第一个分量是红色的,第二个是绿色的,第三个是蓝色的,最后一个分量是alpha。
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

        //1.读取顶点着色器
        String vertexShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.c_vertex_shader);
        //读取片段着色器
        String fragmentShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.c_fragment_shader);
        Log.w(TAG, "onSurfaceCreated,第1步");

        //2.创建顶点着色器
        int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);
        //创建片段着色器
        int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);
        Log.w(TAG, "onSurfaceCreated,第2步");

        //3.连接两个着色器
        program = ShaderHelper.linkProgram(vertexShader, fragmentShader);
        Log.w(TAG, "onSurfaceCreated,第3步");

        //4.验证该对象 program 是否可用
        ShaderHelper.validateProgram(program);
        Log.w(TAG, "onSurfaceCreated,第4步");

        //5.使用自定义的程序来绘制
        GLES20.glUseProgram(program);

        /**
         * 6.获取着色器并且为着色器赋值
         */
        aColorLocation = glGetAttribLocation(program, A_COLOR);
        //获取顶点着色器 a_Position 对象的指针
        aPositionLocation = glGetAttribLocation(program, A_POSITION);
        Log.w(TAG, "onSurfaceCreated,第5步");

        /**
         * 6.为着色器赋值
         */
        vertexData.position(0);//将位置设置在数据的开头处
        glVertexAttribPointer(aPositionLocation, POSITION_COMOPNENT_COUNT,
                GL_FLOAT, false, STRIDE, vertexData);
        //7.指定OpenGL在哪使用顶点数组
        glEnableVertexAttribArray(aPositionLocation);
        Log.w(TAG, "onSurfaceCreated,第6步");

        /**
         * 注释
         * 1.vertexData.position
         * vertexData将位置设置为2,因为读取颜色属性时,要从第一个颜色属性读取,而不是位置属性,
         * 而在原数据中,颜色属性的位置是2
         * 2.glVertexAttribPointer
         * 把颜色数据和着色器中的a_Color关联起来,STRIDE这个参数是跨距,这个值告诉OpenGL两个
         * 颜色属性质检的距离是多少,这样位置属性和颜色属性连接存储,就不会将位置属性当做颜色属性读
         * 3.glEnableVertexAttribArray
         * 指定OpenGL在哪使用顶点数组
         */
        vertexData.position(POSITION_COMOPNENT_COUNT);
        glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_CONT, GL_FLOAT,
                false, STRIDE, vertexData);
        glEnableVertexAttribArray(aColorLocation);
        Log.w(TAG, "onSurfaceCreated,第7步");

    }

    /**
     * 当表面发生变化时,onSurfaceChanged被调用。
     * 这个函数在曲面初始化时至少被调用一次。
     * 请记住,Android通常会在旋转时重启一个活动,在这种情况下,渲染器将被销毁并创建一个新的。
     *
     * @param gl
     * @param width  新的宽度,以像素为单位。
     * @param height 新的高度,以像素为单位。
     */
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        Log.w(TAG, "onSurfaceChanged");
        //设置OpenGL视口填充整个表面。
        glViewport(0, 0, width, height);
    }

    /**
     * 每当需要绘制一个新帧时,OnDrawFrame就会被调用。通常,这是在屏幕的刷新率下完成的。
     *
     * @param gl
     */
    @Override
    public void onDrawFrame(GL10 gl) {
        //清除渲染表面。
        glClear(GL_COLOR_BUFFER_BIT);

        /**
         * 8.绘制页面,画桌子,画线,点;
         * glUniform4f:第一个参数指定Uniform变量的值,后面是红绿蓝和透明值
         * glDrawArrays:第一个参数绘制三角形,第二个参数从数组开头开始读取顶点,第三个参数读到第六个点
         */
        glDrawArrays(GL_TRIANGLE_FAN, 0, 6);

        //绘制线
        glDrawArrays(GL_LINES, 6, 2);

        //画点
        glDrawArrays(GL_POINTS, 8, 1);
        glDrawArrays(GL_POINTS, 9, 1);
    }
}

顶点着色器

attribute vec4 a_Position;
attribute vec4 a_Color;

varying vec4 v_Color;

void main(){
    v_Color = a_Color;

    gl_Position = a_Position;
    gl_PointSize = 10.0;
}

片段着色器

precision mediump float;

varying vec4 v_Color;

void main(){
    gl_FragColor = v_Color;
}

以下为glsl语法中摘录的一部分,可以发现gl_FragColor gl_Position gl_PointSize 均为内部变量,且必须赋值

内置的特殊变量

glsl程序使用一些特殊的内置变量与硬件进行沟通.他们大致分成两种 一种是 input类型,他负责向硬件(渲染管线)发送数据.
另一种是output类型,负责向程序回传数据,以便编程时需要.

在 vertex Shader 中:

output 类型的内置变量:

变量说明单位
highp vec4 gl_Position;gl_Position 放置顶点坐标信息vec4
mediump float gl_PointSize;gl_PointSize 需要绘制点的大小,(只在gl.POINTS模式下有效)float

在 fragment Shader 中:

input 类型的内置变量:

变量说明单位
mediump vec4 gl_FragCoord;片元在framebuffer画面的相对位置vec4
bool gl_FrontFacing;标志当前图元是不是正面图元的一部分bool
mediump vec2 gl_PointCoord;经过插值计算后的纹理坐标,点的范围是0.0到1.0vec2

应用场景


import android.annotation.SuppressLint;
import android.opengl.GLSurfaceView;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;
import com.guide.opengllib.simple.c.CRenderer;

public class Simplectivity extends AppCompatActivity {

    private GLSurfaceView glSurfaceView;
    private GLSurfaceView.Renderer mRenderer;

    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        glSurfaceView = new GLSurfaceView(this);
        mRenderer = new CRenderer(this);
        
		glSurfaceView.setEGLContextClientVersion(2);
        glSurfaceView.setRenderer(mRenderer);
        
        setContentView(glSurfaceView);
    }


    @Override
    protected void onResume() {
        super.onResume();
        glSurfaceView.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        glSurfaceView.onPause();
    }
}

工具ShaderHelper


import android.opengl.GLES20;
import android.util.Log;

import static android.opengl.GLES20.GL_COMPILE_STATUS;
import static android.opengl.GLES20.GL_FRAGMENT_SHADER;
import static android.opengl.GLES20.GL_LINK_STATUS;
import static android.opengl.GLES20.GL_VALIDATE_STATUS;
import static android.opengl.GLES20.GL_VERTEX_SHADER;
import static android.opengl.GLES20.glAttachShader;
import static android.opengl.GLES20.glCompileShader;
import static android.opengl.GLES20.glCreateProgram;
import static android.opengl.GLES20.glCreateShader;
import static android.opengl.GLES20.glDeleteProgram;
import static android.opengl.GLES20.glDeleteShader;
import static android.opengl.GLES20.glGetProgramiv;
import static android.opengl.GLES20.glGetShaderiv;
import static android.opengl.GLES20.glLinkProgram;
import static android.opengl.GLES20.glShaderSource;
import static android.opengl.GLES20.glValidateProgram;

/**
 */
public class ShaderHelper {
    private static final String TAG = "ShaderHelper";

    /** 加载和编译一个顶点着色器,返回OpenGL对象ID */
    public static int compileVertexShader(String shaderCode) {
        return compileShader(GL_VERTEX_SHADER, shaderCode);
    }

    /** 加载和编译一个片段着色器,返回OpenGL对象ID */
    public static int compileFragmentShader(String shaderCode) {
        return compileShader(GL_FRAGMENT_SHADER, shaderCode);
    }

    /** 编译一个着色器,返回OpenGL对象ID */
    private static int compileShader(int type, String shaderCode) {

        //创建一个新的着色器对象
        final int shaderObjectId = glCreateShader(type);

        if (shaderObjectId == 0) {
            Log.w(TAG, "ShaderHelper: Could not create new shader.");
        }

        //传入着色器源
        glShaderSource(shaderObjectId, shaderCode);

        //编译着色器
        glCompileShader(shaderObjectId);

        //获取编译状态
        final int[] compileStatus = new int[1];
        glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);

        //验证编译状态
        if (compileStatus[0] == 0) {
            //如果失败,删除着色器对象
            glDeleteShader(shaderObjectId);
            Log.w(TAG, "ShaderHelper: Compilation of shader failed");
        }

        //返回着色器对象ID
        return shaderObjectId;
    }

    /** 将顶点着色器和片段着色器连接到OpenGL中,返回OpenGL对象ID,连接失败返回0 */
    public static int linkProgram(int vertexShaderId, int fragmentShaderId) {

        //创建一个新的程序对象
        final int programObjectId = glCreateProgram();

        if (programObjectId == 0) {
            Log.w(TAG, "ShaderHelper: Could not create new program");
        }

        //将顶点着色器附加到程序上
        glAttachShader(programObjectId, vertexShaderId);
        //将片段着色器附加到程序上
        glAttachShader(programObjectId, fragmentShaderId);

        //将两个着色器连接到一个程序中
        glLinkProgram(programObjectId);

        //获取连接状态
        final int[] linkStatus = new int[1];
        glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);

        //验证链接状态
        if (linkStatus[0] == 0) {
            //如果失败,删除程序对象
            glDeleteProgram(programObjectId);
            Log.v(TAG, "Results of linking program:\n" + GLES20.glGetProgramInfoLog(programObjectId));
        }

        //返回程序对象ID
        return programObjectId;
    }

    /** 验证OpenGL程序,应该只在开始应用程序调用 */
    public static boolean validateProgram(int programObjectId) {
        glValidateProgram(programObjectId);

        final int[] validateStatus = new int[1];
        glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0);

        return validateStatus[0] != 0;
    }

    public static int buildProgram(String vertexShaderSource, String fragmentShaderSource) {
        int program;
        int vertexShader = compileVertexShader(vertexShaderSource);
        int fragmentShader = compileFragmentShader(fragmentShaderSource);

        program = linkProgram(vertexShader, fragmentShader);

        validateProgram(program);
        return program;
    }
}

工具 TextResourceReader

public class TextResourceReader {
    public static String readTextFileFromResource(Context context, int resourceId) {
        StringBuilder body = new StringBuilder();

        try {
            InputStream inputStream = context.getResources().openRawResource(resourceId);
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String nextLine;
            while ((nextLine = bufferedReader.readLine()) != null) {
                body.append(nextLine);
                body.append('\n');
            }
        } catch (IOException e) {
            throw new RuntimeException("Could not open resource:" + resourceId, e);
        } catch (Resources.NotFoundException nfe) {
            throw new RuntimeException("Resource not found :" + resourceId, nfe);
        }
        return body.toString();
    }
}

效果图如下

glDrawArrays(GL_TRIANGLE_FAN, 0,6);
在这里插入图片描述
我们不妨尝试将第三个参数修改,帮助我们理解gl的绘制原理
glDrawArrays(GL_TRIANGLE_FAN, 0, 1);
在这里插入图片描述

glDrawArrays(GL_TRIANGLE_FAN, 0, 2);
在这里插入图片描述

glDrawArrays(GL_TRIANGLE_FAN, 0, 3);
在这里插入图片描述

glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
在这里插入图片描述

glDrawArrays(GL_TRIANGLE_FAN, 0, 5);
在这里插入图片描述

结论

PS
1. GL_TRIANGLE_FAN绘制最少三个点
2. 绘制时为逆时针方向
3. 过程:读取顶点数据->执行顶点着色器->组装图元->光栅化图元->执行片段着色器->写入帧缓冲区->显示到屏幕;
4. 关于语法可参见 OpenGL手册

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

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

相关文章

Quasi-eccentricity Error Modeling and Compensation in Vision Metrology

论文:Quasi-eccentricity Error Modeling and Compensation in Vision Metrology 中文:视觉计量中准偏心误差建模与补偿 论文地址:Sci-Hub | Quasi-eccentricity error modeling and compensation in vision metrology. Measurement Scienc…

ATA-L系列水声功率放大器——应用场景介绍

ATA-L系列是一款宽频带能输出较大功率的单通道放大器。最大输出1200Vrms电压,6500VA功率,可驱动0~100%的阻性或非阻性负载,客户可根据测试需求灵活调节。 图:ATA-L系列水声功率放大器 国产品牌安泰电子自主研发的ATA-L系列水声功率…

将 Llama2 中文模型接入 FastGPT,再将 FastGPT 接入任意 GPT 套壳应用,真刺激!

FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的问答场景! Llama2 是Facebook 母公司 Meta 发布的开源可商用大模型,国内的…

安防视频监控/视频集中存储/云存储平台EasyCVR平台无法取消共享通道该如何解决?

视频汇聚/视频云存储/集中存储/视频监控管理平台EasyCVR能在复杂的网络环境中,将分散的各类视频资源进行统一汇聚、整合、集中管理,实现视频资源的鉴权管理、按需调阅、全网分发、云存储、智能分析等,视频智能分析平台EasyCVR融合性强、开放度…

〔019〕Stable Diffusion 之 单图中绘制多人分区域写提示词 篇

✨ 目录 🎈 下载区域绘制插件🎈 区域绘制使用🎈 参数讲解和基础使用🎈 Lora 自组🎈 Lora 自组的使用🎈 分区扩散🎈 分区域提示 🎈 下载区域绘制插件 在绘制图片时,经常绘…

服务器数据恢复-vmware ESXI虚拟机数据恢复案例

服务器数据恢复环境: 从物理机迁移一台虚拟机到ESXI,迁移后做了一个快照。该虚拟机上部署了一个SQLServer数据库,存放了5年左右的数据。ESXI上有数十台虚拟机,EXSI连接了一台EVA存储,所有的虚拟机都在EVA存储上。 服务…

问道管理:2023股票印花税是多少钱?2023印花税新规?

在a股市场上,投资者交易股票需求交纳必定的佣钱费用、过户费用以及印花税,其间印花税率和过户费率是固定的,而佣钱费率不同的证券公司有所不同,那么,2023股票印花税是多少钱?2023印花税新规?下面…

【设计模式】Head First 设计模式——桥模式 C++实现

设计模式最大的作用就是在变化和稳定中间寻找隔离点,然后分离它们,从而管理变化。将变化像小兔子一样关到笼子里,让它在笼子里随便跳,而不至于跳出来把你整个房间给污染掉。 设计思想 桥模式。将抽象部分(业务功能)与实现部分(平…

vue项目使用svg实现一个物体沿着线条实时运动;svg图像放大缩小;svg中的文字居中显示

效果如上 html部分 <svg width"500px" height"500px" viewBox"0 0 400 400"><!-- 绘制连线 --><template v-for"(point, index) in points"><line :x1"point.x" :y1"point.y" :x2"in…

联邦学习FedAvg-基于去中心化数据的深度网络高效通信学习

随着计算机算力的提升&#xff0c;机器学习作为海量数据的分析处理技术&#xff0c;已经广泛服务于人类社会。 然而&#xff0c;机器学习技术的发展过程中面临两大挑战&#xff1a;一是数据安全难以得到保障&#xff0c;隐私泄露问题亟待解决&#xff1b;二是网络安全隔离和行业…

【HCIP】18.防火墙

区域隔离&#xff0c;以防火墙的接口为中心定义区域&#xff0c;在防火墙中不同区域互访使用策略来进行控制 NGFW&#xff0c;下一代防火墙&#xff0c;除了是否对他通过进行判断&#xff0c;也可以对安全进行判断&#xff08;例如是否是病毒&#xff0c;DDOS攻击&#xff09;…

常见的下载方式

一. 使用 window.open() 使用场景 // 1. 先封装一个实习下载的函数 export const download (path) > {window.open(下载的接口&#xff0c;例如&#xff1a;/fs/download?path path) } // 2. 使用&#xff1a;在需要下载的地方调用download函数&#xff0c;传入下载的u…

Data Rescue Professional for Mac:专业的数据恢复工具

在数字化时代&#xff0c;我们的生活和工作离不开电脑和存储设备。但是&#xff0c;意外情况时常发生&#xff0c;例如误删除文件、格式化硬盘、病毒攻击等&#xff0c;这些都可能导致重要的数据丢失。面对数据丢失&#xff0c;我们迫切需要一款可靠的数据恢复工具。今天&#…

ASEMI整流桥GBU816的原理和应用

编辑-Z 摘要&#xff1a;整流桥GBU816是一种用于将交流电转换为直流电的电子元器件。本文将从原理、结构、应用以及优点等四个方面对整流桥GBU816进行详细的阐述。 1、整流桥GBU816的原理 整流桥GBU816由四个二极管组成&#xff0c;分别连接在一个桥形电路中。当输入交流电通…

TikTok选品分析:越南7月家电销量第一,这款吸尘器凭什么?

随着经济发展&#xff0c;人们的生活向智能化、便捷化发展&#xff0c;消费者的消费喜好也随之产生变化。家电也不例外&#xff0c;传统吸尘器因其体积较大、清洁不便正逐渐被淘汰。取而代之的是手持吸尘器&#xff0c;其凭借轻便、多功能的特点迅速赢得消费者的喜爱。 过去一…

腾讯云国际代充-GPU服务器安装驱动教程NVIDIA Tesla

腾讯云国际站GPU 云服务器是基于 GPU 的快速、稳定、弹性的计算服务&#xff0c;主要应用于深度学习训练/推理、图形图像处理以及科学计算等场景。 GPU 云服务器提供和标准腾讯云国际 CVM 云服务器一致的方便快捷的管理方式。 GPU 云服务器通过其强大的快速处理海量数据的计算性…

【Python】利用python-docx生成word版本学生花名册

如图&#xff0c;可以用python创建word文档&#xff0c;生成一个学生的花名册。生成的过程&#xff1a;先下载第三方依赖包&#xff0c;安装依赖包&#xff0c;然后引入依赖文件&#xff0c;创建docx文件&#xff0c;添加标题&#xff0c;创建表头&#xff0c;创建表格正文&…

创作纪念日-我的第1024天

机缘 不知不觉已经成为创作者的第1024天啦… … 刚开始接触博客的初衷就是为了记笔记&#x1f4d2;、记总结&#x1f4dd;&#xff0c;或许对于当时就等同于是为了找工作。坚持学习并持续输出博客一年后&#xff0c;这时我发现再写博客&#xff0c;不在是为了找一份工作&…

比亚迪宋L高调亮相成都车展,媒介盒子多家媒体助阵

哈喽,大家好,今天媒介盒子小编又来跟大家分享媒体推广的干货知识了,本篇分享的主要内容是:比亚迪宋L的营销策略。 比亚迪宋L又于2023年8月25日在成都车展上首次亮相&#xff0c;该车将配备比亚迪黑科技中的CTB技术、云辇-C底盘系统和iTAC系统等&#xff0c;预计将在今年第四季…

python教程:如何写类?

目录标题 前言类的定义知识点扩展&#xff1a;构建和初始化1. __ new__(cls,[…)2. __ init__(self,[…)3. __ del__(self) 尾语 前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 类的定义 Python中&#…