美文网首页
3. 实例化-画100个正方体

3. 实例化-画100个正方体

作者: _jetson_ | 来源:发表于2021-04-08 22:20 被阅读0次

    3. 实例化-画100个正方体

    概述

    数据的传递流程

    opengl_bianchengpipe.PNG

    (1)准备顶点属性缓冲区

    • positionVBO:用于存放正方体顶点位置的缓冲区,一个正方体需要24个顶点位置来描述,一个顶点位置三个数(x,y,z)

    • colorVBO:用于存放正方体颜色的缓冲区,每个正方体一种颜色,这里一共有100个正方体,一个颜色四个数(x,y,z,a)

    • mvpVBO:用于存放正方体的mvp变换矩阵的缓冲区,每个正方体一个mvp变换矩阵,这里一共有100个正方体

    • indicesIBO:用于存放正方体顶点顺序的缓冲区,比如它为{0,2,4,6,8,10},那么就从positionVBO中取出下标为{0,2,4,6,8,10}的顶点位置,画两个三角形(一个三角形三个顶点);一个正方体需要12个三角形来描述,所以它为36个indices

    (2)计算mvp矩阵

    (3)将数据传给顶点着色器以及片段着色器来画图

    图形学原理

    1. 齐次坐标

    齐次坐标(x,y,z,w),它是为了兼容点的平移操作,使得我们可以用同一个公式对点和方向作运算。

    (x,y,z,w)同时除于w得到坐标(x/w,y/w,x/w)

    • 当w == 1时,向量(x,y,z,1)为空间中的点

    • 当w == 0时,向量(x,y,z,0)为方向

    齐次坐标主要是兼容点的平移操作,在空间中平移方向是没有意义的:

    qicizuobiao.png

    2. 二维坐标间的转换

    1. 二维旋转矩阵

    把点(x,y)旋转到点(x',y')

    xuanzhuan.PNG
    2. 先平移后旋转,以及先旋转后平移问题

    也就是矩阵乘法的顺序问题,设旋转矩阵为R,平移矩阵为T

    • 先平移后旋转:M = R * T

    • 先旋转后平移:M = T * R

    xuanzhuan_pingyi.PNG
    3. 二维坐标转换

    在xy坐标系中,有一点P(x0,y0),表示的是:点P(x0,y0)相对于xy坐标系原点的值为x0和y0。

    转换到x'y'坐标系之后,变为P(x0',y0'),表示的是:点P(x0',y0')相对于x'y'坐标系原点的值为x0'和y0'

    它们之间的相对位置时不变的,只是换了一种表示方法。

    就比如:小明说,杯子在我的右边;小东说,杯子在我的左边;是一样的道理。这里就是把(杯子在我右边)转换为(杯子在我左边)

    beizi.PNG

    (1)为了将对象描述从xy坐标变换到x'y'坐标,必须建立把x'y'轴叠加到xy轴的变换,这需要分两步进行:

    1. 将x'y'系统的坐标原点(x0,y0)平移到xy系统的原点(0,0);

    2. 将x'轴旋转到x轴上

    所以,就是先平移后旋转:M = R * T

    举例:设x'轴与x轴之间的夹角为45度,x'y'系统的坐标原点为(2,2),将点P(1,1)变换到x'y'系统上,由几何关系可以得到变换后P点坐标的值为(-√2,0)

    zuobiaozhuanhuan_2.PNG

    (2)任何旋转矩阵的元素可以表示为一组正交单位向量的元素

    zhengjiaojuzhen.PNG
    4. 旋转矩阵的逆矩阵

    旋转矩阵的逆矩阵可以通过矩阵转置,或者将旋转角取负值来获得

    nijuzhen.PNG

    3. 三维坐标间转换

    1. 三维坐标绕轴旋转
    juzhenxuanzhuan_3.PNG
    2. 轴角与旋转矩阵

    (1)轴角:绕一个给定轴K(x,y,z)(向量)旋转给定角度。也就是原定坐标轴{A}绕给定向量K(x,y,z)旋转给定角度后,得到坐标系{B}

    注意:向量K(x,y,z)为单位向量

    它的旋转矩阵为:

    image.png zhoujiao_2.PNG

    (2)也可以理解为:一个向量V绕着向量K旋转角,得到向量V(rot)
    公式的推导:详情请见:https://www.bilibili.com/video/BV1h7411c7zK?from=search&seid=5987430286330119296

    推导过程(TODO)

    涉及到:欧拉角,四元数,旋转矩阵,轴角之间的关系

    4. MVP矩阵

    1. 不同的坐标系
    • 局部空间(Local Space,或者称为物体空间(Object Space)):物体坐标系

    • 世界空间(World Space):世界坐标系

    • 观察空间(View Space,或者称为视觉空间(Eye Space)):眼睛坐标系

    • 裁剪空间(Clip Space)

    • 屏幕空间(Screen Space)

    coordinate_systems.png

    由上图可以知道:

    • M:模型矩阵:将物体坐标变换为世界坐标

    • V:视图矩阵:将世界坐标变换为眼睛坐标

    • P:投影矩阵:将眼睛坐标变换为裁剪坐标

    2. 模型矩阵

    将物体坐标变换为世界坐标:

    model_juzhen.PNG
    3. 视图矩阵

    将世界坐标变换为眼睛坐标

    view_juzhen.PNG
    4. 投影矩阵

    将眼睛坐标变换为裁剪坐标

    1. 正交投影
    zhengjiaotouying.png

    将一个上下坐标为t(top)和b(bottom),前后坐标为n(near)和f(far),左右坐标为l(left)和r(right)的正方体:

    1. 将其中心移动到坐标原点

    2. 压缩成边长为2(-1,1)的正方体

    zhengjiaotouying_juzhen.PNG
    2. 齐次坐标不变性

    (x,y,z,1)和(kx,ky,kz,k!=0z)和(xz,yz,z^2,z!=0)在三维空间中,这些都代表的是同一个点(x,y,z)

    例如:(1,0,0,1)和(2,0,0,2)都代表着点(1,0,0)

    3. 透视投影

    将左边的梯形体压缩成右边的长方体

    注意:n和f是不变的,所以,可以得出:

    (1)对于任何在n平面上的点,其坐标的z分量不变

    (2)对于任何在f平面上的点,其坐标的z分量不变

    toushi_touying.png

    从下图可以看出,(x,y,z)坐标和(x',y',z')坐标之间存在相似三角形关系

    touying_juzhen.png toushi_touying_2.PNG
    4. 得出投影矩阵

    投影矩阵就是先做透视投影,把梯形体压缩成一个长方体;然后做正交投影,把这个正方体,放到原点,并压缩成边长为2(-1,1)的正方体

    touying_juzhen_2.PNG

    OpenGL的为啥为负的?(TODO)

    5. 如何求长方体上下左右前后的坐标
    qiuchangkuangao.png

    给出可视角度fovY,nearZ和长宽比aspect(16:9,或者4:3等等),就可以求出长方体的上下左右前后的坐标了

    t(top),b(bottom)= - t,r(right),l(left)= -r,n(nearZ),f(farZ)

    然后代入投影矩阵公式,就可以得出投影矩阵的值了。

    源码解析

    主程序

     #include "esUtil.h"
     #include <stdlib.h>
     #include <math.h>
     #include <android/log.h>
     
     #define NUM_INSTANCES 100
     #define POSITION_LOC 0
     #define COLOR_LOC 1
     #define MVP_LOC 2
     #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "lylesUtil", __VA_ARGS__))
     typedef struct
     {
      GLuint programObject;
     
      GLuint positionVBO;
      GLuint colorVBO;
      GLuint mvpVBO;
      GLuint indicesIBO;
      int numIndices;
      GLfloat angle[NUM_INSTANCES];
     } myUserData;
     
     // 初始化顶点着色器和片段着色器
     // 初始化myUserData里面的数据
     int Init(MYESContext *myesContext)
     {
      GLfloat *positions;
      GLuint *indices;
     
      myUserData *userData = (myUserData *)myesContext->userData;
      char vShaderStr[] =
      "#version 300 es                            \n"
      "layout(location = 0) in vec4 a_position;   \n"
      "layout(location = 1) in vec4 a_color;      \n"
      "layout(location = 2) in mat4 a_mvpMatrix;   \n"
      "out vec4 v_color;                          \n"
      "void main()                                \n"
      "{                                          \n"
      "   v_color = a_color;                      \n"
      "   gl_Position = a_mvpMatrix * a_position; \n"  // 在这里设置mvp变换矩阵
      "}                                          \n";
      char fShaderStr[] =
      "#version 300 es                            \n"
      "precision mediump float;                   \n"
      "in vec4 v_color;                           \n"
      "layout(location = 0) out vec4 outColor;    \n"
      "void main()                                \n"
      "{                                          \n"
      "   outColor = v_color;                     \n"
      "}                                          \n";
      userData->programObject = myesLoadProgram(vShaderStr, fShaderStr);
      // 1\. 生成正方体的position数据和indices数据
      userData->numIndices = myesGenCube(0.1f, &positions, NULL, NULL, &indices);
     
      glGenBuffers(1, &userData->indicesIBO);
      glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, userData->indicesIBO);
      glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint)*userData->numIndices, indices, GL_STATIC_DRAW);
      glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
      free(indices);
     
      glGenBuffers(1, &userData->positionVBO);
      glBindBuffer(GL_ARRAY_BUFFER, userData->positionVBO);
      glBufferData(GL_ARRAY_BUFFER, 24*sizeof(GLfloat)*3, positions, GL_STATIC_DRAW);
      free(positions);
     
      {
      GLubyte colors[NUM_INSTANCES][4];
      int instance;
     
      srandom(0);
     
      for (instance = 0; instance < NUM_INSTANCES; instance++) {
      colors[instance][0] = random() % 255;
      colors[instance][1] = random() % 255;
      colors[instance][2] = random() % 255;
      colors[instance][3] = 0;
      }
     
      glGenBuffers(1, &userData->colorVBO);
      glBindBuffer(GL_ARRAY_BUFFER, userData->colorVBO);
      glBufferData(GL_ARRAY_BUFFER, NUM_INSTANCES*4, colors, GL_STATIC_DRAW);
      }
     
      {
      int instance;
     
      for (instance = 0; instance < NUM_INSTANCES; instance++) {
      userData->angle[instance] = (float) (random() % 32768) / 32767.0f * 360.0f;
      }
     
      glGenBuffers(1, &userData->mvpVBO);
      glBindBuffer(GL_ARRAY_BUFFER, userData->mvpVBO);
      glBufferData(GL_ARRAY_BUFFER, NUM_INSTANCES * sizeof(ESMatrix), NULL, GL_DYNAMIC_DRAW);
      }
      glBindBuffer(GL_ARRAY_BUFFER, 0);
     
      glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
      return GL_TRUE;
     }
     
     // 更新mvp变换矩阵
     void Update(MYESContext *myesContext, float deltaTime)
     {
      myUserData *userData = (myUserData*) myesContext->userData;
      ESMatrix *matrixBuf;
      ESMatrix perspective;
      float aspect;
      int instance = 0;
      int numRows;
      int numColumns;
     
      // 比例=长/高
      aspect = (GLfloat) myesContext->width / (GLfloat)myesContext->height;
      // 先得到一个单位矩阵
      myesMatrixLoadIdentity(&perspective);
      // 然后得到投影矩阵P
      myesPerspective(&perspective, 90.0f, aspect, 0.1f, 100.0f);
      glBindBuffer(GL_ARRAY_BUFFER, userData->mvpVBO);
      matrixBuf = (ESMatrix *)glMapBufferRange(GL_ARRAY_BUFFER, 0, sizeof(ESMatrix) * NUM_INSTANCES, GL_MAP_WRITE_BIT);
      numRows = (int) sqrtf(NUM_INSTANCES);
      numColumns = numRows;
     
      for (instance = 0; instance < NUM_INSTANCES; instance++) {
      ESMatrix modelview;
      float translateX = ((float)(instance % numRows) / (float)numRows)*2.0f - 1.0f;
      float translateY = ((float)(instance/numColumns)/(float)numColumns)*2.0f - 1.0f;
      // 先得到一个单位矩阵
      myesMatrixLoadIdentity(&modelview);
      // 然后将正方体平移到坐标(translateX, translateY, -1.0f),得到模型矩阵M
      myesTranslate(&modelview, translateX, translateY, -1.0f);
      userData->angle[instance] += (deltaTime*40.0f);
      if (userData->angle[instance] >= 360.0f) {
      userData->angle[instance] -= 360.0f;
      }
      // 绕轴(0,0,1)旋转angle角度,得到视图矩阵V,然后和前面的模型矩阵M相乘得到VM矩阵
      myesRotate(&modelview, userData->angle[instance], 0.0, 0, 1.0);
      // 再乘一下,得到VMP矩阵
      myesMatrixMultiply(&matrixBuf[instance], &modelview, &perspective);
      }
     
      glUnmapBuffer(GL_ARRAY_BUFFER);
     }
     
     void Draw(MYESContext *myesContext)
     {
      myUserData *userData = (myUserData *)myesContext->userData;
      // 视口变换
      glViewport(0, 0, myesContext->width, myesContext->height);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      glUseProgram(userData->programObject);
     
      glBindBuffer(GL_ARRAY_BUFFER, userData->positionVBO);
      glVertexAttribPointer(POSITION_LOC, 3, GL_FLOAT, GL_FALSE,
      3*sizeof(GLfloat), (const void *)NULL);
      glEnableVertexAttribArray(POSITION_LOC);
      glBindBuffer(GL_ARRAY_BUFFER, userData->colorVBO);
      glVertexAttribPointer(COLOR_LOC, 4, GL_UNSIGNED_BYTE,
      GL_TRUE, 4*sizeof(GLubyte), (const void *)NULL);
      glEnableVertexAttribArray(COLOR_LOC);
      // void glVertexAttribDivisor (GLuint index, GLuint divisor);
      // 指示OpenGL ES对每个实例(instance)读取一次或者多次顶点属性。
      // divisor为1,表示每个图元实例(每个正方体)读取一次顶点属性,相当于指针P+1这样子
      // divisor为0,则是(每个顶点),读取一次顶点属性
      glVertexAttribDivisor(COLOR_LOC, 1);
     
      glBindBuffer(GL_ARRAY_BUFFER, userData->mvpVBO);
      // 对于4x4矩阵,需要消耗4个顶点属性来存储它们
      glVertexAttribPointer(MVP_LOC + 0, 4, GL_FLOAT, GL_FALSE, sizeof(ESMatrix), (const void *)NULL);
      glVertexAttribPointer(MVP_LOC + 1, 4, GL_FLOAT, GL_FALSE, sizeof(ESMatrix), (const void *)(sizeof(GLfloat)*4));
      glVertexAttribPointer(MVP_LOC + 2, 4, GL_FLOAT, GL_FALSE, sizeof(ESMatrix), (const void *)(sizeof(GLfloat)*8));
      glVertexAttribPointer(MVP_LOC + 3, 4, GL_FLOAT, GL_FALSE, sizeof(ESMatrix), (const void *)(sizeof(GLfloat)*12));
      glEnableVertexAttribArray(MVP_LOC + 1);
      glEnableVertexAttribArray(MVP_LOC + 1);
      glEnableVertexAttribArray(MVP_LOC + 2);
      glEnableVertexAttribArray(MVP_LOC + 3);
     
      glVertexAttribDivisor(MVP_LOC + 0, 1);
      glVertexAttribDivisor(MVP_LOC + 1, 1);
      glVertexAttribDivisor(MVP_LOC + 2, 1);
      glVertexAttribDivisor(MVP_LOC + 3, 1);
     
      glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, userData->indicesIBO);
      //void  glDrawElementsInstanced (GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount);
      // mode表示要渲染的图元;count为绘制的index数量;type为index中的元素索引类型;indices为存放index的地方;
      // instancecount为要绘制的图元实例的数量
      // 用一次API调用,进行多次渲染具有不同属性(例如不同的变换矩阵、颜色、或者大小)的一个对象
      // 这里就是用一次API调用,渲染了100个具有不同变换矩阵、颜色的正方体
      glDrawElementsInstanced(GL_TRIANGLES, userData->numIndices, GL_UNSIGNED_INT, (const void *)NULL, NUM_INSTANCES);
     }
     
     void Shutdown(MYESContext *myesContext)
     {
      myUserData *userData = (myUserData *)myesContext->userData;
     
      glDeleteBuffers(1, &userData->positionVBO);
      glDeleteBuffers(1, &userData->colorVBO);
      glDeleteBuffers(1, &userData->mvpVBO);
      glDeleteBuffers(1, &userData->indicesIBO);
      glDeleteProgram(userData->programObject);
     }
     
     
     int myesMain(MYESContext *myesContext)
     {
      myesContext->userData = malloc(sizeof(myUserData));
      // 这个width和height是没有用的,userData中的width和height是用的是系统中的
      myesCreateWindow(myesContext, "Example 7-1 Instancing", 640, 480, MY_ES_WINDOW_RGB | MY_ES_WINDOW_DEPTH);
     
      if (!Init(myesContext)) {
      return GL_FALSE;
      }
     
      esRegisterShutdownFunc(myesContext, Shutdown);
      esRegisterUpdateFunc(myesContext, Update);
      esRegisterDrawFunc(myesContext, Draw);
     
      return GL_TRUE;
     }
    

    变换矩阵函数

    // 生成单位矩阵
     void myesMatrixLoadIdentity(ESMatrix *result)
     {
      memset(result, 0x0, sizeof(ESMatrix));
      result->m[0][0] = 1.0f;
      result->m[1][1] = 1.0f;
      result->m[2][2] = 1.0f;
      result->m[3][3] = 1.0f;
     }
     
     // 生成投影矩阵的
     void myesPerspective(ESMatrix *result, float fovy, float aspect, float nearZ, float farZ)
     {
      GLfloat frustumW, frustumH;
     
      frustumH = tanf(fovy / 360.0f * PI) * nearZ;
      frustumW = frustumH * aspect;
     
      myesFrustum(result, -frustumW, frustumW, -frustumH, frustumH, nearZ, farZ);
     }
     
     void myesFrustum(ESMatrix *result, float left, float right, float bottom, float top, float nearZ, float farZ)
     {
      float deltaX = right - left;
      float deltaY = top - bottom;
      float deltaZ = farZ - nearZ;
      ESMatrix frust;
     
      if ((nearZ <= 0.0f) || (farZ <= 0.0f) ||
      (deltaX <= 0.0f) || (deltaY <= 0.0f) || (deltaZ <= 0.0f)) {
      return;
      }
     
      frust.m[0][0] = 2.0f * nearZ / deltaX;
      frust.m[0][1] = frust.m[0][2] = frust.m[0][3] = 0.0f;
     
      frust.m[1][1] = 2.0f * nearZ / deltaY;
      frust.m[1][0] = frust.m[1][2] = frust.m[1][3] = 0.0f;
     
      frust.m[2][0] = (right + left) / deltaX;
      frust.m[2][1] = (top + bottom) / deltaY;
      frust.m[2][2] = - (nearZ + farZ) / deltaZ;
      frust.m[2][3] = -1.0f;
     
      frust.m[3][2] = -2.0f * nearZ * farZ / deltaZ;
      frust.m[3][0] = frust.m[3][1] = frust.m[3][3] = 0.0f;
     
      myesMatrixMultiply(result, &frust, result);
     }
     
     // 矩阵乘法
     void myesMatrixMultiply ( ESMatrix *result, ESMatrix *srcA, ESMatrix *srcB )
     {
      ESMatrix    tmp;
      int         i;
     
      for ( i = 0; i < 4; i++ )
      {
      tmp.m[i][0] =  ( srcA->m[i][0] * srcB->m[0][0] ) +
      ( srcA->m[i][1] * srcB->m[1][0] ) +
      ( srcA->m[i][2] * srcB->m[2][0] ) +
      ( srcA->m[i][3] * srcB->m[3][0] ) ;
     
      tmp.m[i][1] =  ( srcA->m[i][0] * srcB->m[0][1] ) +
      ( srcA->m[i][1] * srcB->m[1][1] ) +
      ( srcA->m[i][2] * srcB->m[2][1] ) +
      ( srcA->m[i][3] * srcB->m[3][1] ) ;
     
      tmp.m[i][2] =  ( srcA->m[i][0] * srcB->m[0][2] ) +
      ( srcA->m[i][1] * srcB->m[1][2] ) +
      ( srcA->m[i][2] * srcB->m[2][2] ) +
      ( srcA->m[i][3] * srcB->m[3][2] ) ;
     
      tmp.m[i][3] =  ( srcA->m[i][0] * srcB->m[0][3] ) +
      ( srcA->m[i][1] * srcB->m[1][3] ) +
      ( srcA->m[i][2] * srcB->m[2][3] ) +
      ( srcA->m[i][3] * srcB->m[3][3] ) ;
      }
     
      memcpy ( result, &tmp, sizeof ( ESMatrix ) );
     }
     
     // 平移矩阵
     void myesTranslate ( ESMatrix *result, GLfloat tx, GLfloat ty, GLfloat tz )
     {
      result->m[3][0] += ( result->m[0][0] * tx + result->m[1][0] * ty + result->m[2][0] * tz );
      result->m[3][1] += ( result->m[0][1] * tx + result->m[1][1] * ty + result->m[2][1] * tz );
      result->m[3][2] += ( result->m[0][2] * tx + result->m[1][2] * ty + result->m[2][2] * tz );
      result->m[3][3] += ( result->m[0][3] * tx + result->m[1][3] * ty + result->m[2][3] * tz );
     }
     
     // 轴角,旋转矩阵
     void myesRotate ( ESMatrix *result, GLfloat angle, GLfloat x, GLfloat y, GLfloat z )
     {
      GLfloat sinAngle, cosAngle;
      GLfloat mag = sqrtf ( x * x + y * y + z * z );
     
      sinAngle = sinf ( angle * PI / 180.0f );
      cosAngle = cosf ( angle * PI / 180.0f );
     
      if ( mag > 0.0f )
      {
      GLfloat xx, yy, zz, xy, yz, zx, xs, ys, zs;
      GLfloat oneMinusCos;
      ESMatrix rotMat;
     
      x /= mag;
      y /= mag;
      z /= mag;
     
      xx = x * x;
      yy = y * y;
      zz = z * z;
      xy = x * y;
      yz = y * z;
      zx = z * x;
      xs = x * sinAngle;
      ys = y * sinAngle;
      zs = z * sinAngle;
      oneMinusCos = 1.0f - cosAngle;
     
      rotMat.m[0][0] = ( oneMinusCos * xx ) + cosAngle;
      rotMat.m[0][1] = ( oneMinusCos * xy ) - zs;
      rotMat.m[0][2] = ( oneMinusCos * zx ) + ys;
      rotMat.m[0][3] = 0.0F;
     
      rotMat.m[1][0] = ( oneMinusCos * xy ) + zs;
      rotMat.m[1][1] = ( oneMinusCos * yy ) + cosAngle;
      rotMat.m[1][2] = ( oneMinusCos * yz ) - xs;
      rotMat.m[1][3] = 0.0F;
     
      rotMat.m[2][0] = ( oneMinusCos * zx ) - ys;
      rotMat.m[2][1] = ( oneMinusCos * yz ) + xs;
      rotMat.m[2][2] = ( oneMinusCos * zz ) + cosAngle;
      rotMat.m[2][3] = 0.0F;
     
      rotMat.m[3][0] = 0.0F;
      rotMat.m[3][1] = 0.0F;
      rotMat.m[3][2] = 0.0F;
      rotMat.m[3][3] = 1.0F;
     
      myesMatrixMultiply ( result, &rotMat, result );
      }
     }
    
    juzhen_zongjie.PNG image.png

    参考

    1. 第三课:矩阵
    http://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-3-matrices/
    2. 旋转矩阵
    https://zh.wikipedia.org/wiki/%E6%97%8B%E8%BD%AC%E7%9F%A9%E9%98%B5
    3. CG07-3D变换和欧拉角/轴角/四元数
    https://www.bilibili.com/video/BV1h7411c7zK?from=search&seid=5987430286330119296
    4. GAMES101-现代计算机图形学入门-闫令琪
    https://www.bilibili.com/video/BV1X7411F744?p=4
    5. 坐标系统
    https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/08%20Coordinate%20Systems/
    6. View Transform(视图变换)详解
    https://www.cnblogs.com/graphics/archive/2012/07/12/2476413.html
    

    相关文章

      网友评论

          本文标题:3. 实例化-画100个正方体

          本文链接:https://www.haomeiwen.com/subject/ychbkltx.html