OpenGL--变换

来源:互联网 发布:被驯服的象 抄袭 知乎 编辑:程序博客网 时间:2024/05/22 03:35

目录(?)[+]

变换

向量

向量有一个方向(Direction)和大小(Magnitude,也叫做强度或长度)。向量可以在任意维度(Dimension)上。由于向量表示的是方向,起始于何处并不会改变它的值。我们通常设定这个方向的原点为(0,0,0),然后指向对应坐标的点,使其变为位置向量(Position Vector)来表示(你也可以把起点设置为其他的点。

向量与标量运算

标量(Scalar)只是一个数字(或者说是仅有一个分量的矢量)。当把一个向量加/减/乘/除一个标量,我们可以简单的把向量的每个分量分别进行该运算。

向量取反

我们在一个向量的每个分量前加负号就可以实现取反了(或者说用-1数乘该向量)。

向量加减

向量的加法可以被定义为是分量的(Component-wise)相加,即将一个向量中的每一个分量加上另一个向量的对应分量。 
两个向量的相减会得到这两个向量指向位置的差。

长度

我们可以通过勾股定理来计算出向量的长度。有一个特殊类型向量叫做单位向量(Unit Vector)。单位向量有一个特别的性质——它的长度是1。我们可以用任意向量的每个分量除以向量的长度得到它的单位向量,我们把这种方法叫做一个向量的标准化(Normalizing)。

向量相乘

点乘 
两个向量的点乘(Dot Product)等于它们的数乘结果乘以两个向量之间夹角的余弦值。 
点乘只和两个向量的角度有关。你也许记得当90度的余弦是0,0度的余弦是1。使用点乘可以很容易测试两个向量是否正交(Orthogonal)或平行(正交意味着两个向量互为直角)。 
点乘是按对应分量逐个相乘,然后再把结果相加。 
叉乘 
叉乘(Cross Product)只在3D空间有定义,它需要两个不平行向量作为输入,生成正交于两个输入向量的第三个向量。 
(Ax,Ay,Az)×(Bx,By,Bz)= (Ay⋅Bz−Az⋅By,Az⋅Bx−Ax⋅Bz,Ax⋅By−Ay⋅Bx)

矩阵

矩阵简单说是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。矩阵可以通过(i, j)进行索引,i是行,j是列,也叫做矩阵的维度(Dimension)。

矩阵的加减

标量值要加到矩阵的每一个元素上。矩阵与标量的减法也是同样的。 
矩阵与矩阵之间的加减就是两个矩阵对应元素的加减运算,所以总体的规则和与标量运算是差不多的,只不过在相同索引下的元素才能进行运算。这也就是说加法和减法只在同维度的矩阵中是有定义的。一个3×2矩阵和一个2×3矩阵(或一个3×3矩阵与4×4矩阵)是不能进行加减的。同样的法则也适用于减法。

矩阵的数乘

和矩阵与标量的加减一样,矩阵与标量之间的乘法也是矩阵的每一个元素分别乘以该标量。

矩阵相乘

矩阵乘法基本上意味着遵照规定好的法则进行相乘。当然,相乘还有一些限制: 
只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘。 
矩阵相乘不遵守交换律(Commutative),A⋅B≠B⋅A。 
两个矩阵相乘的结果是左侧矩阵对应的行向量和右侧矩阵对应的列向量的点乘积所组成的新的矩阵。

矩阵和向量相乘

向量基本上就是一个N×1矩阵,N是向量分量的个数(也叫N维(N-dimensional)向量)。如果我们有一个M×N矩阵,我们可以用这个矩阵乘以我们的N×1向量,因为我们的矩阵的列数等于向量的行数,所以它们就能相乘。

单位矩阵

我们能想到的最简单的变换矩阵就是单位矩阵(Identity Matrix)。单位矩阵是一个除了对角线以外都是0的N × N矩阵。

缩放

当我们对一个向量进行缩放(Scaling)的时候就是对向量的长度进行缩放,而它的方向保持不变。如果我们进行2或3维操作,那么我们可以分别定义一个有2或3个缩放变量的向量,每个变量缩放一个轴(x、y或z)。 
我们可以尝试去缩放向量¯v=(3,2)。我们可以把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一;我们可以沿着y轴把向量的高度缩放为原来的两倍。我们刚刚的缩放操作是不均匀(Non-uniform)缩放,因为每个轴的缩放因子(Scaling Factor)都不一样。如果每个轴的缩放都一样那么就叫均匀缩放(Uniform Scale)。 
如果我们把缩放变量表示为(S1,S2,S3)我们可以为任意向量(x,y,z)定义一个缩放矩阵: 
这里写图片描述

平移

平移(Translation)是在原来向量的基础上加上另一个的向量从而获得一个在不同位置的新向量的过程,这样就基于平移向量移动(Move)了向量。 
如果我们把缩放向量表示为(Tx,Ty,Tz)我们就能把平移矩阵定义为: 
这里写图片描述 
齐次坐标(Homogeneous coordinates) 
向量的w分量也叫齐次坐标。想要从齐次坐标得到3D坐标,我们可以把x、y和z坐标除以w坐标。我们通常不会注意这个问题,因为w分量通常是1.0。使用齐次坐标有几点好处:它允许我们在3D向量上进行平移(如果没有w分量我们是不能平移向量的),下一章我们会用w值创建3D图像。 
如果一个向量的齐次坐标是0,这个坐标就是方向向量(Direction Vector),因为w坐标是0,这个向量就不能平移(译注:这也就是我们说的不能平移一个方向)。

旋转

2D或3D空间中点的旋转用角(Angle)来表示。角可以是角度制或弧度制的,周角是360度或2 PI弧度。 
大多数旋转函数需要用弧度制的角,但是角度制的角也可以很容易地转化为弧度制: 
弧度转角度:角度 = 弧度 * (180.0f / PI) 
角度转弧度:弧度 = 角度 * (PI / 180.0f) 
PI约等于3.14159265359。 
在3D空间中旋转需要一个角和一个旋转轴(Rotation Axis)。 
这里写图片描述

矩阵的组合

使用矩阵变换的真正力量在于,根据矩阵之前的乘法,我们可以把多个变换组合到一个矩阵中。 
注意,当矩阵相乘时我们先写平移再写缩放变换的。矩阵乘法是不可交换的,这意味着它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个乘以向量的,所以你应该从右向左读这个乘法。我们建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是平移,否则它们会(消极地)互相影响。

GLM

下载地址:https://sourceforge.net/projects/ogl-math/?source=typ_redirect 
把头文件的根目录复制到你的includes文件夹,然后你就可以使用这个库了。 
我们需要的GLM的大多数功能都可以从下面这3个头文件中找到:

#include <glm/glm.hpp>#include <glm/gtc/matrix_transform.hpp>#include <glm/gtc/type_ptr.hpp>
  • 1
  • 2
  • 3

让我们来旋转和缩放之前教程中的那个箱子。首先我们把箱子逆时针旋转90度。然后缩放0.5倍,使它变成原来的二分之一。我们先来创建变换矩阵:

glm::mat4 trans;trans = glm::rotate(trans, 90.0f, glm::vec3(0.0, 0.0, 1.0));trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
  • 1
  • 2
  • 3

首先,我们把箱子在每个轴缩放到0.5倍,然后沿Z轴旋转90度。注意有纹理的那面矩形是在XY平面上的,我们需要把它绕着z轴旋转。因为我们把这个矩阵传递给了GLM的每个函数,GLM会自动将矩阵相乘,返回的结果是一个包括了多个变换的变换矩阵。

有些GLM版本接收的是弧度而不是角度,这种情况下你可以用glm::radians(90.0f)将角度转换为弧度。 
下一个大问题是:如何把矩阵传递给着色器?我们在前面简单提到过GLSL里的mat4类型。所以我们改写顶点着色器来接收一个mat4的uniform变量,然后再用矩阵uniform乘以位置向量:

#version 330 corelayout(location = 0) in vec3 position;layout(location = 1) in vec3 color;layout(location = 2) in vec2 texCoord;out vec3 ourColor;out vec2 TexCoord;uniform mat4 transform;void main(){    gl_Position = transform * vec4(position, 1.0f);    ourColor = color;    TexCoord = vec2(texCoord.x, 1.0 - texCoord.y);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

GLSL也有mat2和mat3类型从而允许了像向量一样的混合运算。前面提到的所有数学运算(比如标量 - 矩阵乘法,矩阵 - 向量乘法和矩阵 - 矩阵乘法)在矩阵类型里都可以使用。当出现特殊的矩阵运算的时候我们会特别说明发生了什么的。 
在把位置向量传给gl_Position之前,我们添加一个uniform,并且用变换矩阵乘以它。我们的箱子现在应该是原来的二分之一大小并旋转了90度(向左倾斜)。当然,我们仍需要把变换矩阵传递给着色器:

GLuint transformLoc = glGetUniformLocation(ourShader.Program, “transform”); 
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans)); 
我们首先请求uniform变量的地址,然后用有Matrix4fv后缀的glUniform函数把矩阵数据发送给着色器。第一个参数你现在应该很熟悉了,它是uniform的地址(Location)。第二个参数告诉OpenGL我们将要发送多少个矩阵,目前是1。第三个参数询问我们我们是否希望对我们的矩阵进行置换(Transpose),也就是说交换我们矩阵的行和列。OpenGL开发者通常使用一种内部矩阵布局叫做以列为主顺序的(Column - major Ordering)布局。GLM已经是用以列为主顺序定义了它的矩阵,所以并不需要置换矩阵,我们填GL_FALSE、最后一个参数是实际的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望的那种,因此我们要先用GLM的自带的函数value_ptr来变换这些数据。

转载请注明出处:http://blog.csdn.net/ylbs110/article/details/52269424

示例

我们继续在之前的示例上进行修改。 
代码

#include <iostream>using namespace std;// GLEW#define GLEW_STATIC#include <GL/glew.h>// GLFW#include <GLFW/glfw3.h>// SOIL#include <SOIL\SOIL.h>#include <glm\glm.hpp>#include <glm\gtc\matrix_transform.hpp>#include <glm\gtc\type_ptr.hpp>#include "Shader.h"const GLuint WIDTH = 800, HEIGHT = 600;void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);GLuint loadTexture(string fileName, GLint REPEAT, GLint FILTER);// Shadersconst GLchar* vertexShaderSource = "#version 330 core\n""layout (location = 0) in vec3 position;\n"//顶点数据传入的坐标"layout (location = 1) in vec3 color;\n"//顶点数据传入的颜色"layout (location = 2) in vec2 texCoord;\n"//顶点数据传入的颜色"uniform vec4 offset;\n""uniform float mixPar;\n""uniform mat4 transform;\n""out vec3 Color;\n""out vec2 TexCoord;\n""out vec4 vertexColor;\n"//将顶点坐标作为颜色传入片段着色器,测试所得效果"out float MixPar;\n""void main()\n""{\n""gl_Position =transform* vec4(position.x, position.y, position.z, 1.0)+offset;\n""vertexColor=gl_Position;\n""Color=color;\n""TexCoord=texCoord;\n""MixPar=mixPar;\n""}\0";const GLchar* fragmentShaderSourceOrange = "#version 330 core\n""out vec4 color;\n""in vec4 vertexColor;\n""in vec3 Color;\n""in vec2 TexCoord;\n""uniform sampler2D ourTexture;\n""void main()\n""{\n""color =texture(ourTexture, TexCoord)+vertexColor;\n"//将顶点颜色和坐标转换的颜色进行混合"}\n\0";const GLchar* fragmentShaderSourceYellow = "#version 330 core\n""out vec4 color;\n""in vec4 vertexColor;\n""in vec3 Color;\n""in vec2 TexCoord;\n""in float MixPar;\n""uniform sampler2D ourTexture1;\n""uniform sampler2D ourTexture2;\n""void main()\n""{\n""color =mix(texture(ourTexture1, TexCoord),texture(ourTexture2, vec2(TexCoord.x,1-TexCoord.y)),MixPar)+vec4(Color, 1.0f)+vertexColor;\n"//合成两张纹理并对第二张纹理进行翻转操作,混合比例由上下键控制"}\n\0";Shader yellowShader,orangeShader;//两种shaderGLuint texContainer, texAwesomeface;//纹理idfloat key_UD=0.5f;//混合比例GLuint VBO1, VAO1;GLuint VBO2, VAO2, EBO2;void shaderInit() {    yellowShader = Shader(vertexShaderSource, fragmentShaderSourceYellow);    orangeShader = Shader(vertexShaderSource, fragmentShaderSourceOrange);}void textureInit() {    texContainer = loadTexture("container.jpg", GL_CLAMP_TO_EDGE, GL_LINEAR);    texAwesomeface = loadTexture("awesomeface.png", GL_MIRRORED_REPEAT, GL_NEAREST);}GLuint loadTexture(string fileName,GLint REPEAT, GLint FILTER) {    //创建纹理    GLuint texture;    glGenTextures(1, &texture);    //绑定纹理    glBindTexture(GL_TEXTURE_2D, texture);    // 为当前绑定的纹理对象设置环绕、过滤方式    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, REPEAT);    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, REPEAT);    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, FILTER);    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, FILTER);    //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);    //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);    // 加载纹理    int width, height;    unsigned char* image = SOIL_load_image(fileName.c_str(), &width, &height, 0, SOIL_LOAD_RGB);    // 生成纹理    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);    glGenerateMipmap(GL_TEXTURE_2D);    //释放图像的内存并解绑纹理对象    SOIL_free_image_data(image);    glBindTexture(GL_TEXTURE_2D, 0);    return texture;}void vertexObjectInit() {    //不使用索引缓冲对象用两个三角形绘制一个梯形    // 设置顶点缓存和属性指针    GLfloat vertices1[] = {        //位置                    //颜色:黄色        -0.5f, 0.2f, 0.0f,      1.0f, 1.0f, 0.0f,       0.0f, 0.0f,// BottomLeft          0.5f, 0.2f, 0.0f,       1.0f, 1.0f, 0.0f,       2.0f, 0.0f,// BottomRight         -0.2f,  0.5f, 0.0f,     1.0f, 1.0f, 0.0f,       0.0f, 2.0f,// TopLeft         0.5f, 0.2f, 0.0f,       1.0f, 1.0f, 0.0f,       2.0f, 0.0f,// BottomRight         -0.2f,  0.5f, 0.0f,     1.0f, 1.0f, 0.0f,       0.0f, 2.0f,// TopLeft        0.2f,  0.5f, 0.0f,      1.0f, 1.0f, 0.0f,       2.0f, 2.0f// TopRight    };    //创建索引缓冲对象    glGenBuffers(1, &VBO1);    glGenVertexArrays(1, &VAO1);    glBindVertexArray(VAO1);    // 把顶点数组复制到缓冲中供OpenGL使用    glBindBuffer(GL_ARRAY_BUFFER, VBO1);    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices1), vertices1, GL_STATIC_DRAW);    // 位置属性    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);    glEnableVertexAttribArray(0);    // 颜色属性    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));    glEnableVertexAttribArray(1);    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));    glEnableVertexAttribArray(2);    glBindBuffer(GL_ARRAY_BUFFER, 0);// 这个方法将顶点属性指针注册到VBO作为当前绑定顶点对象,然后我们就可以安全的解绑    glBindVertexArray(0);// 解绑 VAO (这通常是一个很好的用来解绑任何缓存/数组并防止奇怪错误的方法)                         // 使用索引缓冲对象用两个三角形绘制一个长方形    GLfloat vertices[] = {        //位置                    //颜色:橙色        -0.5f, 0.2f, 0.0f,      0.5f, 0.25f, 0.1f,      -1.0f, 2.0f,// TopLeft          0.5f, 0.2f, 0.0f,       0.5f, 0.25f, 0.1f,      2.0f, 2.0f,// TopRight         -0.5f,  -0.5f, 0.0f,    0.5f, 0.25f, 0.1f,      -1.0f, -1.0f,// BottomLeft           0.5f,  -0.5f, 0.0f,     0.5f, 0.25f, 0.1f,      2.0f, -1.0f// BottomRight     };    GLuint indices[] = {        0,1,2,        1,2,3    };    glGenBuffers(1, &VBO2);    glGenBuffers(1, &EBO2);    glGenVertexArrays(1, &VAO2);    glBindVertexArray(VAO2);    glBindBuffer(GL_ARRAY_BUFFER, VBO2);    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO2);    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);    glEnableVertexAttribArray(0);    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));    glEnableVertexAttribArray(1);    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));    glEnableVertexAttribArray(2);    glBindBuffer(GL_ARRAY_BUFFER, 0);    glBindVertexArray(0);}int main(){    //初始化GLFW    glfwInit();    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);    //创建窗口对象    GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);    if (window == nullptr)    {        std::cout << "Failed to create GLFW window" << std::endl;        glfwTerminate();        return -1;    }    glfwMakeContextCurrent(window);    //注册键盘回调    glfwSetKeyCallback(window, key_callback);    //初始化GLEW    glewExperimental = GL_TRUE;    if (glewInit() != GLEW_OK)    {        std::cout << "Failed to initialize GLEW" << std::endl;        return -1;    }    //告诉OpenGL渲染窗口尺寸大小    int width, height;    glfwGetFramebufferSize(window, &width, &height);    glViewport(0, 0, width, height);    //初始化并绑定shaders    shaderInit();    //初始化textures    textureInit();    //初始化顶点对象数据    vertexObjectInit();    //让窗口接受输入并保持运行    while (!glfwWindowShouldClose(window))    {        //检查事件        glfwPollEvents();        //渲染指令        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);        glClear(GL_COLOR_BUFFER_BIT);        //设置根据时间变换的x,y偏移值,最终效果为圆周运动        GLfloat timeValue = glfwGetTime();        GLfloat offsetx = (sin(timeValue) / 2) + 0.5;        GLfloat offsety = (cos(timeValue) / 2) + 0.5;        glBindTexture(GL_TEXTURE_2D, texContainer);        //绘制梯形        orangeShader.Use();        // 更新uniform值        GLint vertexColorLocation = glGetUniformLocation(orangeShader.Program, "offset");        glUniform4f(vertexColorLocation, offsetx, offsety, 0.0f, 1.0f);        glm::mat4 trans1;        trans1 = glm::translate(trans1, glm::vec3(0.5f, -0.5f, 0.0f));        trans1 = glm::rotate(trans1, (GLfloat)glfwGetTime() * 5.0f, glm::vec3(0.0f, 0.0f, 1.0f));        GLuint transformLoc1 = glGetUniformLocation(orangeShader.Program, "transform");        glUniformMatrix4fv(transformLoc1, 1, GL_FALSE, glm::value_ptr(trans1));        glBindVertexArray(VAO1);        glDrawArrays(GL_TRIANGLES, 0, 6);        glBindVertexArray(0);        //绘制长方形             yellowShader.Use();        //绑定两张贴图        glActiveTexture(GL_TEXTURE0);        glBindTexture(GL_TEXTURE_2D, texContainer);        glUniform1i(glGetUniformLocation(yellowShader.Program, "ourTexture1"), 0);        glActiveTexture(GL_TEXTURE1);        glBindTexture(GL_TEXTURE_2D, texAwesomeface);        glUniform1i(glGetUniformLocation(yellowShader.Program, "ourTexture2"), 1);        // 更新uniform值        //设置运动轨迹        //GLint vertexorangeLocation = glGetUniformLocation(yellowShader.Program, "offset");        //glUniform4f(vertexorangeLocation, offsetx, offsety, 0.0f, 1.0f);        //设置混合比例        GLint mixPar = glGetUniformLocation(yellowShader.Program, "mixPar");        glUniform1f(mixPar, key_UD);        //设置变换矩阵        glm::mat4 trans2;               trans2 = glm::translate(trans2, glm::vec3(-0.5f, 0.8f, 0.0f));        trans2 = glm::scale(trans2, (sin((GLfloat)glfwGetTime())+1.0f)*0.5f* glm::vec3(1.0f, 1.0f, 0.0f));          GLuint transformLoc2 = glGetUniformLocation(yellowShader.Program, "transform");        glUniformMatrix4fv(transformLoc2, 1, GL_FALSE, glm::value_ptr(trans2));        glBindVertexArray(VAO2);        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);        glBindVertexArray(0);        //交换缓冲        glfwSwapBuffers(window);    }    // Properly de-allocate all resources once they've outlived their purpose    glDeleteVertexArrays(1, &VAO1);    glDeleteBuffers(1, &VBO1);    glDeleteVertexArrays(1, &VAO2);    glDeleteBuffers(1, &VBO2);    //释放资源    glfwTerminate();    return 0;}void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode){    // 当用户按下ESC键,我们设置window窗口的WindowShouldClose属性为true    // 关闭应用程序    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)        glfwSetWindowShouldClose(window, GL_TRUE);    if (key == GLFW_KEY_UP&& action == GLFW_PRESS)//按下UP键增加混合比例        key_UD = key_UD + 0.1f;    if (key == GLFW_KEY_DOWN&& action == GLFW_PRESS)//按下DOWN减小混合比例        key_UD = key_UD - 0.1f;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306

结果 
我们可以看到屋顶在右侧旋转,而墙体在左上角不断变大变小。 
这里写图片描述

原创粉丝点击