`
啸笑天
  • 浏览: 3462044 次
  • 性别: Icon_minigender_1
  • 来自: China
社区版块
存档分类
最新评论

OpenGL = Hello World

 
阅读更多

 OpenGL中的大多数函数使用了一种 基于状态 的方法,大多数OpenGL对象都需要在使用前把该对象绑定到context上。这里有两个新名词——OpenGL对象和Context

 

Context

Context是一个非常抽象的概念,我们姑且把它理解成一个包含了所有OpenGL状态的对象。如果我们把一个Context销毁了,那么OpenGL也不复存在。

 

OpenGL对象

我们可以把OpenGL对象理解成一个状态的集合,它负责管理它下属的所有状态。当然,除了状态,OpenGL对象还会存储其他数据。注意。这些状态和上述context中的状态并不重合,只有在把一个OpenGL对象绑定到context上时,OpenGL对象的各种状态才会映射到context的状态。因此,这时如果我们改变了context的状态,那么也会影响这个对象,而相反地,依赖这些context状态的函数也会使用存储在这个对象上的数据。

因此,OpenGL对象的绑定既可能是为了修改该对象的状态(大多数对象需要绑定到context上才可以改变它的状态),也可能是为了让context渲染时使用它的状态。

 

画了一个图,仅供理解。图中灰色的方块代表各种状态,箭头表示当把一个OpenGL对象绑定到context上后,对应状态的映射。



 

 

 

 

OpenGL就是一个 “状态机” 。那些各种各样的API调用会改变这些状态,或者根据这些状态进行操作。但我们要注意的是,这只是说明了OpenGL是怎样被定义的,但硬件是否是按状态机实现的就是另一回事了。不过,这不是我们需要担心的地方。

OpenGL对象 包含了下面一些类型:Buffer ObjectsVertex Array ObjectsTexturesFramebuffer Objects等等。我们下面会讲到 Vertex Array Objects这个对象。

这些对象都有三个相关的重要函数:

        void glGen*(GLsizei n, GLuint *objects);

负责生成一个对象的name。而name就是这个对象的引用。

void glDelete*(GLsizei n, const GLuint *objects);

负责销毁一个对象。

void glBind*(GLenum target, GLuint object);

将对象绑定到context上。

关于OpenGL对象还有很多内容,这里就不讲了。可以参见 官方wiki  

在开始第一个程序之前,我们还要了解一些图形名词。

渲染(Rendering :计算机从模型到创建一张图像的过程。OpenGL仅仅是其中一个渲染系统。它是一个基于光栅化的系统,其他的系统还有光线追踪(但有时也会用到OpenGL)等。

模型(Models)或者对象(Objects :这里两者的含义是一样的。指从几何图元——点、线、三角形中创建的东西,由顶点指定。

Shaders :这是一类特殊的函数,是在图形硬件上执行的。 我们可以理解成,Shader是一些为图形处理单元(GPU)编译的小程序。OpenGL包含了编译工具来把我们编写的Shader源代码编译成可以在GPU上运行的代码。在OpenGL中,我们可以使用四种shader阶段。最常见的就是vertex shaders——它们可以处理顶点数据;以及fragment shaders,它们处理光栅化后生成的fragments vertex shadersfragment shaders是每个OpenGL程序必不可少的部分。

像素(pixel :像素是我们显示器上的最小可见元素。我们系统中的像素被存储在一个帧缓存(framebuffer)中。帧缓存是一块由图形硬件管理的内存空间,用于供给给我们的显示设备。

 

OpenGL坐标系

OpenGL坐标系不同于UIKit坐标系,其实它是这样的



 

 

除了方向,还有一点需要注意,默认情况各个方向坐标值范围为(-11

 

 

需要补充一点,默认情况下,GLKViewController渲染RunLoop并非NSRunLoopCommonModes,而是NSDefaultRunLoopMode,因此在UIKit中使用GLKViewController,当滑动界面时,OpenGL是不会渲染的。

可以自定义修改:

#import <UIKit/UIKit.h>

#import <GLKit/GLKView.h>

 

@class CADisplayLink;

 

@interface HJGLKViewController : UIViewController

 

@property (nonatomic, readonly) GLKView *glkView;

 

@property (nonatomic) NSInteger preferredFramesPerSecond;

 

@property (nonatomic, getter=isPaused) BOOL paused;

 

@end

 

 

 

#import "HJGLKViewController.h"

 

static const NSInteger HJGLKDefaultFramesPerSecond = 30;

 

@interface HJGLKViewController () <GLKViewDelegate>

 

@property (nonatomic, strong) GLKView       *glkView;

@property (nonatomic, strong) CADisplayLink *displayLink;

 

@end

 

@implementation HJGLKViewController

 

- (void)dealloc {

    self.paused = YES;

}

 

- (void)viewDidLoad {

    [super viewDidLoad];

    

    self.view.backgroundColor = [UIColor whiteColor];

    self.glkView = [[GLKView alloc] initWithFrame:self.view.frame];

    self.glkView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

    self.glkView.delegate = self;

    [self.view addSubview:self.glkView];

    

    self.preferredFramesPerSecond = HJGLKDefaultFramesPerSecond;

    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView)];

    self.displayLink.frameInterval = MAX(1, 60.0f / _preferredFramesPerSecond);

    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

}

 

#pragma mark -

- (void)setPreferredFramesPerSecond:(NSInteger)preferredFramesPerSecond {

    _preferredFramesPerSecond = preferredFramesPerSecond;

    self.displayLink.frameInterval = MAX(1, 60.0f / _preferredFramesPerSecond);

}

 

- (BOOL)isPaused {

    return self.displayLink.paused;

}

 

- (void)setPaused:(BOOL)paused {

    self.displayLink.paused = paused;

}

 

- (void)drawView {

    [self update];

    [self.glkView display];

}

 

- (void)update {

    

}

 

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {

    

}

 

@end

 

 

 

HelloWorld的实现过程:

1. 控制器

 

: openGL里面很多的属性值和宏都是在ios的基础上加了GL前缀,所以当你看到GL_TRUE, GL_FLOAT的时候,其实他就是true float 别被吓到。

 

 

为了简便省时,我决定直接在ViewController中修改代码,首先我们先导入GLKit.h头文件,紧接着那个将ViewController的类型修改为GLKViewController.

 

 

 

然后在Main.storyboard修改ViewControllerview的类型为GLKView.如图所示.

 

 

 

上面的准备工作已经是做完了,那么接下来,就是正题部分了,我们现在ViewController.m中声明两个属性.一个是OpenGL ES 上下文属性的EAGLContext对象,一个是矩阵相关的GLKBaseEffect对象.

2. 两个控件

//openGL渲染上下文, 请类比CGContextRef

@property(nonatomic,strong)EAGLContext *mContext;

//这个东西主要负责视觉效果

@property(nonatomic,strong)GLKBaseEffect *mEffect;

 

通过官方的API文档,我们知道,EAGLContext对象管理一个OpenGL ES渲染环境状态信息,命令,以及使用OpenGL ES的所需要资源。OpenGL ES执行任何命令之前,都需要通过EAGLContext对象来实现。同时官方文档也提到,绘制一个上下文之前,你必须完成framebuffer对象绑定到上下文。

GLKBaseEffect这个类实现了OpenGL ES 1.0公共(common)的shading行为,简化(从1.0)到OpenGL ES 2.0的转化。它们也提供了让光和纹理(lighting and texturing)工作的简单方法。GLKBaseEffect对象提供了着色器这一功能.其实着色器应该算的上是OpenGL ES的一大特色,但是GLKBaseEffect已经包含了这一功能,相当于封装了着色器.现在只是知道有着色器就行.

 

3. 初始化,配置OpenGL ES 上下文信息

 //设置当前使用的context使用的api版本

    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

    //设置当前的context

    [EAGLContext setCurrentContext:self.context];

 

    //设置视图的渲染属性

    GLKView *view = (GLKView *)self.view;

    view.context = self.context;

    //颜色渲染格式

    view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;//默认值可以不设置

    //模板渲染格式

    view.drawableDepthFormat = GLKViewDrawableDepthFormat24;

 

 

    //初始化视觉管理对象

    self.effect = [[GLKBaseEffect alloc] init];

    //光照效果

    self.effect.light0.enabled = GL_TRUE;

    //设置光照颜色 RGBA格式

    self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 1.0f, 1.0f, 1.0f);

 

 

iOS支持的OpenGL版本

typedef NS_ENUM(NSUInteger, EAGLRenderingAPI)

{

    kEAGLRenderingAPIOpenGLES1 = 1,

    kEAGLRenderingAPIOpenGLES2 = 2,

    kEAGLRenderingAPIOpenGLES3 = 3,

};

 

4. 图形

因为本文的目标是实现一个简单的静态图形,所以构造如下一个图形

    //从左到右, 依次是  顶点X, Y, Z, 法线X, Y, Z, 纹理S, T

    //顶点位置用于确定在什么地方显示,法线用于光照模型计算,纹理则用在贴图中。

    GLfloat squareVertexData[48] = {

        0.5f, 0.5f, -0.9f,      0.0f, 0.0f, 1.0f,       1.0f, 1.0f,

        -0.5f, 0.5f, -0.9f,     0.0f, 0.0f, 1.0f,       0.0f, 1.0f,

        0.5f, -0.5f, -0.9f,     0.0f, 0.0f, 1.0f,       1.0f, 0.0f,

        0.5f, -0.5f, -0.9f,     0.0f, 0.0f, 1.0f,       1.0f, 0.0f,

        -0.5f, 0.5f, -0.9f,     0.0f, 0.0f, 1.0f,       0.0f, 1.0f,

        -0.5f, -0.5f, -0.9f,    0.0f, 0.0f, 1.0f,       0.0f, 0.0f,

    };

GLfloat的一个数组,其实就是一个 float的数组,数组中的值对应的概念看注释
注意,此时手机/模拟器的原点是在屏幕的中心位置,我们先搞平面效果,所以暂时不考虑Z
只关注每一行的前2个坐标点. 可以在坐标轴上大概画出这个图形,是一个正方形
至于为什么会有重复点,在OpenGL ES只能绘制三角形,不能绘制多边形,但是在OpenGL中确实可以直接绘制多边形.

纹理坐标系的取值范围是[0, 1],原点是在左下角。故而点(0, 0)在左下角,点(1, 1)在右上角。

 

5.设置渲染缓冲区

设置一个渲染的缓冲区域,概念不清楚的请类比于一个缓存, context对象要从这个缓存里面拿数据来进行渲染.

    //声明一个缓冲区的标识(GLuint类型)

    GLuint buffer;

    //OpenGL自动分配一个缓冲区空间

    glGenBuffers(1, &buffer);

    //绑定这个缓冲区到当前“Context

    glBindBuffer(GL_ARRAY_BUFFER, buffer);

    //将我们前面预先定义的顶点数据“squareVertexData”复制进这个缓冲区中。

    //注:参数“GL_STATIC_DRAW”,它表示此缓冲区内容只能被修改一次,但可以无限次读取。

    glBufferData(GL_ARRAY_BUFFER, sizeof(squareVertexData), squareVertexData, GL_STATIC_DRAW);

一个与当先context绑定的缓冲区被分配出来,接下来要渲染什么图形,只需要把对应的数据放到缓冲区,这样context会在程序运行的时候不断地从缓冲区拿数据然后渲染到界面上.

 

 

glGenBuffers(GLsizei n,GLuint *buffers):任何非零的无符合整数都可以作为缓冲区对象的标识符使用。这个函数的作用就是向系统申请n个缓冲区,系统把这n个缓冲区的标识符都放进buffers数组中。还可以调用glIsBuffer()函数判断一个标识符是否正被使用。
例如,glGenBuffers(1, &index);这是是向系统申请1个缓冲区,标识符为index.

glBindBuffer(GLenum target, GLuint buffer) :把这个缓冲区绑定给顶点还是索引.通俗点,也就是定义了这个缓冲区存储的是什么.target用于决定绑定的是顶点数据(GL_ARRAY_BUFFER)还是索引数据(GL_ELEMENT_ARRAY_BUFFER).

glBufferData (GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage):CPU中的内存中的数组复制到GPU的内存中.target用于决定绑定的是顶点数据(GL_ARRAY_BUFFER)还是索引数据(GL_ELEMENT_ARRAY_BUFFER).size决定数据的存储长度.data则是数据信息.usage表示数据的读写方式,是一个枚举类型,这里使用的是GL_STATIC_DRAW,它表示此缓冲区内容只能被修改一次,但可以无限次读取。

 

 

6. 填充数据进缓冲区

上述声明只是告诉context要渲染一个名字叫做squareVertexData的图形,但是具体怎么渲染并没有告诉context,接下来要做的就是告诉context如何渲染,注意:索引数据不需要复制到通用顶点属性中.

因为图形坐标包括了位置,法线,纹理. 所以要分别把三部分添加到context的缓冲区.

    /**

     填充数据,参数含义分别为:

 

     顶点属性索引(这里是位置/法线/纹理)、

     3个分量的矢量、

     类型是浮点(GL_FLOAT)、

     填充时不需要单位化(GL_FALSE)、

     在数据数组中每行的跨度是32个字节(4*8=32。从预定义的数组中可看出,每行有8GL_FLOAT浮点值,而GL_FLOAT4个字节,因此每一行的跨度是4*8)。

     最后一个参数是一个偏移量的指针,用来确定“第一个数据”将从内存数据块的什么地方开始。

     */

 

 

    //位置

    glEnableVertexAttribArray(GLKVertexAttribPosition);

    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 4*8, (char *)NULL + 0);

 

    //法线

    glEnableVertexAttribArray(GLKVertexAttribNormal);

    glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, 4*8, (char *)NULL + 12);

 

    //纹理

    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);

    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, 4*8, (char *)NULL + 24);

    //加载纹理内容

    //GLKit加载纹理,默认都是把坐标设置在“左上角”。然而,OpenGL的纹理贴图坐标却是在左下角,这样刚好颠倒。

    NSDictionary *options = @{ GLKTextureLoaderOriginBottomLeft : @YES };

    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"222.jpg" ofType:nil];

    GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:options error:nil];

    self.effect.texture2d0.enabled = GL_TRUE;

    self.effect.texture2d0.name = textureInfo.name;

 

7. 开始渲染

准备工作搞定,剩下的就是要开始渲染了.

//渲染代码放在这里

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {

    glClearColor(0.3f, 0.6f, 1.0f, 1.0f);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

 

    [self.effect prepareToDraw];

    glDrawArrays(GL_TRIANGLES, 0, 6);

 

 

}

//场景变化放在这里

- (void)update {

 

    //修改投影矩阵

    //加下面的代码,是因为如果不加,默认的屏幕长宽比和openGL的长宽比不一致,会导致有一个方向被拉伸

    CGSize size = self.view.bounds.size;

    //算出屏幕的纵横比

    float aspect = fabs(size.width / size.height);

 

    GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0), aspect, 0.1f, 10.0f);

    self.effect.transform.projectionMatrix = projectionMatrix;

 

    GLKMatrix4 modelViewMatrix = GLKMatrix4Translate(GLKMatrix4Identity, 0.0f, 0.0f, -1.0f);

    self.effect.transform.modelviewMatrix = modelViewMatrix;

}

 

 

 

  • glClearColor (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) : 渲染前的“清除”操作,指定在清除屏幕之后填充什么样的颜色.四个参数就是RGB.
  • glClear (GLbitfield mask) :指定需要清除的缓冲.mask指定缓冲的类型.可以使用 | 运算符组合不同的缓冲标志位,表明需要清除的缓冲.可以使用以下标识符.

GL_COLOR_BUFFER_BIT: 当前可写的颜色缓冲

 

GL_DEPTH_BUFFER_BIT: 深度缓冲

 

GL_ACCUM_BUFFER_BIT: 累积缓冲

 

GL_STENCIL_BUFFER_BIT: 模板缓冲

 

 

[self.mEffect prepareToDraw];这个就是启动当前GLKBaseEffect对象.

  • glDrawElements (GLenum mode, GLsizei count, GLenum type, const GLvoid* indices) : 通过顶点索引绘制图像.mode指定的绘制的类型.类型展示如下,这里使用的是GL_TRIANGLES,count指定的顶点索引数组中元素的个数,type 为索引数组(indices)中元素的类型,只能是下列值之一: GL_UNSIGNED_BYTE ,GL_UNSIGNED_SHORT,GL_UNSIGNED_INT. indices指向索引数组的指针。

GL_POINTS:  单独的将顶点画出来。

 

GL_LINES:  单独地将直线画出来。

 

GL_LINE_STRIP:  连贯地将直线画出来。

 

GL_LINE_LOOP:  连贯地将直线画出来。行为和GL_LINE_STRIP类似,但是会自动将最后一个顶点和第一个顶点通过直线连接起来。

 

GL_TRIANGLES:这个参数意味着OpenGL使用三个顶点来组成图形。所以,在开始的三个顶点,将用顶点1,顶点2,顶点3来组成一个三角形。完成后,在用下一组的三个顶点(顶点456)来组成三角形,直到数组结束。

 

GL_TRIANGLE_STRIP:  OpenGL的使用将最开始的两个顶点出发,然后遍历每个顶点,这些顶点将使用前2个顶点一起组成一个三角形。

 

 

GL_TRIANGLE_FAN:  在跳过开始的2个顶点,然后遍历每个顶点,让OpenGL将这些顶点于它们前一个,以及数组的第一个顶点一起组成一个三角形。

 

 

http://www.cocoachina.com/game/20141127/10335.html

http://www.olinone.com/?p=308

http://www.jianshu.com/p/e04edce75fbf

http://www.jianshu.com/p/353b5496e494

http://www.jianshu.com/notebooks/2135411/latest

  • 大小: 50.6 KB
  • 大小: 15.4 KB
分享到:
评论
1 楼 啸笑天 2016-11-09  
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (GLfloat *)NULL +3);

相关推荐

    opengl的helloworld

    "OpenGL的HelloWorld"通常是指一个简单的程序,它使用OpenGL库来在屏幕上显示文本“Hello, World!”,以此作为学习OpenGL编程的起点。 在开始之前,你需要确保已经安装了必要的开发环境,包括OpenGL库、GLUT...

    openGL hello world

    "OpenGL Hello World"通常是指使用OpenGL库编写的一个简单的示例程序,它标志着开发者开始接触并理解OpenGL的基本概念和操作。这个"OpenGL红宝书"指的是经典的《OpenGL Programming Guide》,这本书是学习OpenGL的...

    openGL helloworld xcode版本

    "OpenGL HelloWorld"通常是初学者接触这个图形库时编写的第一个程序,它旨在展示如何设置基本的OpenGL环境并绘制出简单的图形。在这个Xcode版本的OpenGL HelloWorld中,我们将深入探讨如何在苹果的开发环境中配置和...

    Hello Window OpenGL 第一个窗口

    在这个"Hello Window OpenGL 第一个窗口"的教程中,我们将探讨如何创建并显示一个基本的OpenGL窗口。 首先,你需要了解的是OpenGL并不直接处理窗口系统。在Windows系统中,我们通常使用GLUT(OpenGL Utility ...

    openGL helloworld

    根据提供的信息,我们可以总结出以下关于“OpenGL HelloWorld”程序的关键知识点: ### 1. OpenGL简介 OpenGL(Open Graphics Library)是一种用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。...

    OpenGL写的hello窗体

    在本例中,可能是简单的"hello, world"文本,或者是一个几何形状,如三角形。 5. **顶点坐标和投影**:在OpenGL中,我们需要定义顶点坐标,并可能使用视口变换和投影矩阵来将3D坐标转换为2D屏幕坐标。 6. **帧缓冲...

    webgl-3d-helloworld

    "webgl-3d-helloworld"项目是一个基础教程或示例,教你如何使用JavaScript和WebGL创建一个简单的3D“Hello, World!”程序。 在描述中提到的"javascript 实现的 3d helloworld",意味着这个项目将通过JavaScript代码...

    安卓 OpenGL ES 2.0 完全入门(一):基本概念和 hello world.docx

    本文将详细介绍安卓 OpenGL ES 2.0 的基本概念和 hello world 项目,帮助读者快速入门 OpenGL ES 2.0 的世界。 基本概念 OpenGL ES 2.0 是一种用于移动设备的图形处理 API,能够提供高性能的图形处理能力。OpenGL ...

    glsl_HelloWorld.rar_C# glsl_GLSL_glsl openGL_glsl_HelloWorld_op

    一个用opengl着色语言(glsl)编写的犹他壶,并且可以通过鼠标点动沿三个坐标轴转动

    android游戏引擎libGDX 实例和源码gdx-helloworld-0.9.1,很不错的,强烈推荐

    在本实例中,"gdx-helloworld-0.9.1"是一个基于LibGDX的游戏入门项目,非常适合初学者学习和理解LibGDX的基本用法。 LibGDX的核心组件包括以下几个部分: 1. **Backend**:LibGDX提供了不同平台的后端支持,如...

    vulkan hello world伪代码

    "Vulkan Hello World"是学习Vulkan API的基础教程,通过一个简单的程序来展示Vulkan的基本用法和3D渲染流程。下面我们将详细讨论Vulkan API的使用以及"Vulkan Hello World"中的关键概念。 1. **Vulkan API基础**:...

    opengl.rar_world

    "opengl.rar_world"这个标题暗示我们讨论的是如何在QT环境下用C++实现一个基础的OpenGL“Hello, World!”程序。 首先,要开始使用OpenGL,我们需要在QT项目中包含必要的库。这通常通过在.pro文件中添加`QT += ...

    IOS入门之HelloWorld源代码

    // Hello World_Code // #import #import "AppDelegate.h" int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }...

    vvvv的演示程序:HELLO WORLD

    【vvvv的演示程序:HELLO WORLD】 vvvv是一款基于节点图界面的视觉编程语言,主要应用于实时图像处理、互动艺术以及多媒体应用开发。在这个"HELLO WORLD"演示程序中,我们将深入探讨vvvv的基本概念、工作流程以及...

    Hello-GLUT:一个非常简单的“ Hello World!” GLUT应用程序演示了如何使用MinGW和MSVC用C编写OpenGL应用程序

    Hello-GLUT:一个非常简单的“ Hello World!” GLUT应用程序演示了如何使用MinGW和MSVC用C编写OpenGL应用程序

    hello-gl:OpenGL 2.0中的“ Hello World”

    hello-gl:OpenGL 2.0中的“ Hello World”

    3D游戏编程:OpenGL入门.pdf

    OpenGL的“Hello World”示例通常涉及到以下几个步骤: 1. **初始化OpenGL环境**:设置OpenGL版本、启用必要的扩展等。 2. **创建窗口**:使用GLUT或其他窗口库创建一个窗口。 3. **定义渲染函数**:编写一个简单的...

    java openGL 开发库

    学习JOGL时,可以从简单的“Hello, World”示例开始,逐步扩展到更复杂的项目。例如,创建一个旋转的立方体,展示基本的图形绘制和变换操作。随着技能的提升,可以尝试实现光照、纹理映射、动画效果等。 **7. 社区...

Global site tag (gtag.js) - Google Analytics