当前位置:   article > 正文

【games101】作业笔记——作业3_games101笔记

games101笔记

这里我第一篇写的文章就是作业3,因为作业3包含了整个1-3作业的代码,除了作业代码本身之外,我也会写一些额外自己的拓展

obj文件读取 (拓展部分)

图中的牛是以obj文件格式给出,直接在win下双击打开会得到这个模型。
在这里插入图片描述
而实际上,用文本编辑器打开是文本格式的文件。

其中#的部分是注释

####
#
# OBJ File Generated by Meshlab
#
####
# Object spot_triangulated_good.obj
#
# Vertices: 3225
# Faces: 5856
#
####
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

后面的一部分定义了一系列的点,其中v代表一个点,vn是一个法向量,vt是纹理坐标

vn 0.713667 0.093012 -0.694283
vt 0.854030 0.663650
v 0.348799 -0.334989 -0.083233
vn 0.742238 0.092067 0.663782
vt 0.724960 0.675077
v 0.313132 -0.399051 0.881192
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里说明,第一个v是0.348799 -0.334989 -0.083233,第二个v是 0.313132 -0.399051 0.881192

第三部分定义了一系列面,这里的面都是由三角形组成的。

f 739/739/739 735/735/735 736/736/736
f 189/189/189 736/736/736 735/735/735
f 192/192/192 738/738/738 737/737/737
f 739/739/739 737/737/737 738/738/738
f 190/190/190 741/741/741 740/740/740
f 743/743/743 740/740/740 741/741/741
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

其中f代表一个面,一个面由三个vertex组成,每个vertex包含纹理坐标、坐标和法线方向三个部分。比如第一行 f 的第一个点是739/739/739,第一个数是顶点坐标id,第二个纹理坐标id,第三个数是 法向量id。注意id的编号是v vn vt分别各自按顺序编号,编号值从1开始。

投影矩阵

投影矩阵是一个很有争议的地方,我自己调的时候经常上下颠倒或者左右颠倒,使用我这里做了这个处理。首先把main函数中的正数改成负数。

    if (command_line)
    {
        r.clear(rst::Buffers::Color | rst::Buffers::Depth);
        r.set_model(get_model_matrix(angle));
        r.set_view(get_view_matrix(eye_pos));
        r.set_projection(get_projection_matrix(45.0, 1, -0.1, -50));//把源代码的正改成负
        r.draw(TriangleList);
        cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
        image.convertTo(image, CV_8UC3, 1.0f);
        cv::cvtColor(image, image, cv::COLOR_RGB2BGR);

        cv::imwrite(filename, image);

        return 0;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我的投影矩阵采用这个形式:

Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
    // TODO: Use the same projection matrix from the previous assignments
    Eigen::Matrix4f projection = Eigen::Matrix4f::Zero();
    eye_fov = eye_fov * MY_PI / 180;
    float t = -zNear * tan(eye_fov / 2);
    float r = t * aspect_ratio;
    projection(0, 0) = zNear / r;
    projection(1, 1) = zNear / t;
    projection(2, 2) = (zFar + zNear) / (zNear - zFar);
    projection(2, 3) = 2 * zFar * zNear / (zFar - zNear);
    projection(3, 2) = 1;
    return projection;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我采用了课上的矩阵

[ z N e a r / r 0 0 0 0 z N e a r / t 0 0 0 0 ( z F a r + z N e a r ) ( z N e a r − z F a r ) 2 ∗ z F a r ∗ z N e a r ( z F a r − z N e a r ) 0 0 1 0 ] \left[

zNear/r0000zNear/t0000(zFar+zNear)(zNearzFar)2zFarzNear(zFarzNear)0010
\right] zNear/r0000zNear/t0000(zNearzFar)(zFar+zNear)100(zFarzNear)2zFarzNear0

通过两个特殊点看矩阵的效果。
矩阵会把(*, *, zNear, 1)映射到(*, *, zNear, zNear),经过齐次除法,z=1。
把(*, *, zFar, 1)映射到(*, *, -zFar, zFar),经过齐次除法,z = -1。

注意此时zFar是更负的,也就是更小的。利用这个矩阵映射,原来比较小的z,(zFar < zNear),在映射后依然会比较小(-1 < 1)

rasterize和z-buffer

接下来,需要完成void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos) 函数中的内容。

有几个问题我想做一下重点的记录。

物体z值大小与物体的前后关系

首先看一下投影后z值大小与物体的前后关系。在经过model和view两个变换后,我们发现物体的z,这里称为 z 0 z_0 z0,均为负,且 z 0 z_0 z0越大(越接近0)则物体越靠近相机。经过上面的透视投影矩阵,z的大小相对关系不变。

在函数rst::rasterizer::draw中,出现如下代码,又一次修改了透视投影后的z。

vert.z() = vert.z() * f1 + f2
  • 1

此时f1是正值,也就是说依然不改变z的相对大小。综上,z越大,物体离相机越近。

insideTriangle函数声明

该函数输入的x,y是int类型的,但是考虑到像素中心 i + 0.5 的情况,手动把它改成float类型的,不然会出错。

透视矫正插值

代码中对三角形深度的差值使用了透视矫正插值,推导见这个博客

我当时有个疑惑,为什么要用这么复杂的方式,既然插值要使用透视投影之前,在相机空间中的坐标进行插值,那么为什么不把相机空间的坐标存下来,直接在这个函数计算alpha, beta和gamma呢?

这是因为,我们把三角形的相机空间坐标存下来是很容易的,但是,我们要插值的点的x,y坐标是用屏幕空间(i + 0.5, j + 0.5 ) 表示的,如何为插值点找到原始的相机空间x,y坐标呢?其实这个反推相机坐标空间的过程就是透视矫正插值的思路。

对于纹理坐标等,我也在代码中实现了透视矫正插值,但是好像结果没有太大的影响。

代码的实现如下:

void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos) 
{
    auto v = t.toVector4();
    int l = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
    int r = std::max(std::max(v[0].x(), v[1].x()), v[2].x()) + 1;
    int d = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
    int top = std::max(std::max(v[0].y(), v[1].y()), v[2].y()) + 1;
    // TODO : Find out the bounding box of current triangle.
    // iterate through the pixel and find if the current pixel is inside the triangle

    for (int x = l; x <= r; x++) {
        for (int y = d; y <= top; y++) {

            // vector<vector<double>> pList = {{x + 0.5, y + 0.5} };
            // auto [alpha, beta, gamma] = computeBarycentric2D(x + 0.5, y + 0.5, t.v);
            if (insideTriangle(x + 0.5, y + 0.5, t.v )){
                auto [alpha, beta, gamma] = computeBarycentric2D(x + 0.5, y + 0.5, t.v);
                float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                z_interpolated *= w_reciprocal;

                int idx = get_index(x, y);
                if (depth_buf[idx] < z_interpolated) {
                    depth_buf[idx] = z_interpolated;

                    auto interpolated_color = interpolate(alpha, beta, gamma, static_cast<Eigen::Vector3f>(t.color[0] / v[0].w()), t.color[1] / v[1].w(), t.color[2] / v[2].w(), 1 / w_reciprocal);
                    Vector3f interpolated_normal = interpolate(alpha, beta, gamma, static_cast<Eigen::Vector3f>(t.normal[0] / v[0].w()), t.normal[1] / v[1].w(), t.normal[2] / v[2].w(), 1 / w_reciprocal).normalized();
                    auto interpolated_texcoords = interpolate(alpha, beta, gamma, static_cast<Eigen::Vector2f>(t.tex_coords[0] / v[0].w()), t.tex_coords[1] / v[1].w(), t.tex_coords[2] / v[2].w(), 1 / w_reciprocal);
              
                    /*auto interpolated_color = interpolate(alpha, beta, gamma, t.color[0], t.color[1], t.color[2], 1 / w_reciprocal);
                    auto interpolated_normal = interpolate(alpha, beta, gamma, t.normal[0], t.normal[1], t.normal[2], 1 / w_reciprocal);*/
                    // auto interpolated_texcoords = interpolate(alpha, beta, gamma, t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1);
                    
                    
                    //这里的interpolated_shadingcoords要使用view_pos的坐标而不是t.v的坐标,因为t.v是经过了mvp变换加上视口变换的像素坐标,而view_pos是以相机为原点的真实坐标                   
                    auto interpolated_shadingcoords = interpolate(alpha, beta, gamma, static_cast<Eigen::Vector3f>(view_pos[0] / v[0].w()), view_pos[1] / v[1].w(), view_pos[2] / v[2].w(), 1 / w_reciprocal);
                    auto tx = interpolated_shadingcoords.x();
                    auto ty = interpolated_shadingcoords.y();
                    auto tz = interpolated_shadingcoords.z();
                    //0.881108 0.50367 -9.70347
                    // 1.36768 0.428212


                    // auto interpolated_shadingcoords = interpolate(alpha, beta, gamma, view_pos[0] , view_pos[1], view_pos[2], 1 / w_reciprocal);
                    fragment_shader_payload payload( interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
                    payload.view_pos = interpolated_shadingcoords;
                    auto pixel_color = fragment_shader(payload);
                    set_pixel({x,y}, pixel_color);
                }


            }
        }
    }
    // TODO: From your HW3, get the triangle rasterization code.
    // TODO: Inside your rasterization loop:
    //    * v[i].w() is the vertex view space depth value z.
    //    * Z is interpolated view space depth for the current pixel
    //    * zp is depth between zNear and zFar, used for z-buffer

    // float Z = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
    // float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
    // zp *= Z;

    // TODO: Interpolate the attributes:
    // auto interpolated_color
    // auto interpolated_normal
    // auto interpolated_texcoords
    // auto interpolated_shadingcoords

    // Use: fragment_shader_payload payload( interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
    // Use: payload.view_pos = interpolated_shadingcoords;
    // Use: Instead of passing the triangle's color directly to the frame buffer, pass the color to the shaders first to get the final color;
    // Use: auto pixel_color = fragment_shader(payload);

 
}
  • 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

这是使用命令行参数 normal 跑出来的结果

在这里插入图片描述

phong shading的代码实现

Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload)
{
    Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
    Eigen::Vector3f kd = payload.color;
    Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

    auto l1 = light{{20, 20, 20}, {500, 500, 500}};
    auto l2 = light{{-20, 20, 0}, {500, 500, 500}};

    std::vector<light> lights = {l1, l2};
    Eigen::Vector3f amb_light_intensity{10, 10, 10};
    Eigen::Vector3f eye_pos{0, 0, 10};

    float p = 150;

    Eigen::Vector3f color = payload.color;
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;

    Eigen::Vector3f result_color = {0, 0, 0};
    result_color += ka.cwiseProduct(amb_light_intensity);
    for (auto& light : lights)
    {
        // TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular* 
        // components are. Then, accumulate that result on the *result_color* object.
        auto l = (light.position - point).normalized();
        auto r2 = (light.position - point).dot(light.position - point);
        auto v = (eye_pos - point).normalized();
        auto h = (v + l).normalized();

        auto Ls = ks.cwiseProduct(light.intensity / (r2)) * std::pow(std::max(0.0f, normal.dot(h)), p);
        auto Ld = kd.cwiseProduct(light.intensity / (r2)) * std::max(0.0f, normal.dot(l));

        result_color = result_color + Ls + Ld;
        
    }

    return result_color * 255.f;
}
  • 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

注意几个点:

  1. 求Ls 的时候要注意指数p
  2. 法向量normal要经过归一化,我这里选择在插值的时候就进行归一化操作,也可以在shading函数里再进行归一化。
  3. ka.cwiseProduct(amb_light_intensity);向量-向量按元素相乘,用的是cwiseProduct这个成员函数。

结果:
在这里插入图片描述

纹理的实现

    Eigen::Vector3f return_color = {0, 0, 0};
    if (payload.texture)
    {
        // TODO: Get the texture value at the texture coordinates of the current fragment
        float u = payload.tex_coords.x();
        float v = payload.tex_coords.y();
        return_color = payload.texture->getColor(u, v);

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这部分比较简单,直接查纹理坐标然后设上去就好了。效果如下:
在这里插入图片描述

bump mapping的实现

这一段我参考了其他人的做法,但是觉得有点问题。

这里我有两个不理解的地方,感觉不符合逻辑:

  1. TBN空间是一个局部的切向空间,t 、b、n应该是相互正交的,但是按代码里的做法。

t = ( x ∗ y x 2 + z 2 , x 2 + z 2 , z ∗ y x 2 + z 2 ) ( 1 ) t = (\frac{x*y} {\sqrt{x^2 + z^2}}, \sqrt{x^2 + z^2}, \frac{z*y} {\sqrt{x^2 + z^2}}) (1) t=(x2+z2 xy,x2+z2 ,x2+z2 zy)(1)

n = ( x , y , z ) n = (x, y, z) n=(x,y,z)

t ⋅ n = 1 x 2 + z 2 ( x 2 y + x 2 y + z 2 y + z 2 y ) t \cdot n = \frac{1}{\sqrt{x^2 + z^2}} (x^2y + x^2y + z^2y + z^2y) tn=x2+z2 1(x2y+x2y+z2y+z2y)

很显然不为0,而如果把 t 的第二项加个符号,变为
t 2 = ( x ∗ y x 2 + z 2 , − x 2 + z 2 , z ∗ y x 2 + z 2 ) ( 2 ) t_2 = (\frac{x*y} {\sqrt{x^2 + z^2}}, - \sqrt{x^2 + z^2}, \frac{z*y} {\sqrt{x^2 + z^2}}) (2) t2=(x2+z2 xy,x2+z2 ,x2+z2 zy)(2)
t 2 ⋅ n = 1 x 2 + z 2 ( x 2 y − x 2 y − z 2 y + z 2 y ) = 0 t_2 \cdot n = \frac{1}{\sqrt{x^2 + z^2}} (x^2y - x^2y - z^2y + z^2y) = 0 t2n=x2+z2 1(x2yx2yz2y+z2y)=0

如果按照第二种是正交的,但是跑出来的结果和答案不一样。

  1. dU 的求法用的是payload.texture->getColor(u + 1.0f/w, v).norm()去进行计算,用的是模长。我看这个文章结合理解,法线贴图是用RGB通道来表示XYZ坐标吧,这里有人给出解答吗?

除了两个地方,还有几个注意点:

  1. payload.texture->getColor(u + 1.0f/w, v)给出下一个u的时候,1要除以w,竖直方向同理,因为这里的纹理坐标是[0,1]如果直接+1就会超过范围上限而报错。

按照代码注释的 t 向量的计算方法(1)式,结果如下:
在这里插入图片描述
按照(2)式的结果
在这里插入图片描述

代码实现如下:

Eigen::Vector3f bump_fragment_shader(const fragment_shader_payload& payload)
{

    Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
    Eigen::Vector3f kd = payload.color;
    Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

    auto l1 = light{ {20, 20, 20}, {500, 500, 500} };
    auto l2 = light{ {-20, 20, 0}, {500, 500, 500} };

    std::vector<light> lights = { l1, l2 };
    Eigen::Vector3f amb_light_intensity{ 10, 10, 10 };
    Eigen::Vector3f eye_pos{ 0, 0, 10 };

    float p = 150;

    Eigen::Vector3f color = payload.color;
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;


    float kh = 0.2, kn = 0.1;

    // TODO: Implement bump mapping here
    // Let n = normal = (x, y, z)
    // Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
    // Vector b = n cross product t
    // Matrix TBN = [t b n]
    // dU = kh * kn * (h(u+1/w,v)-h(u,v))
    // dV = kh * kn * (h(u,v+1/h)-h(u,v))
    // Vector ln = (-dU, -dV, 1)
    // Normal n = normalize(TBN * ln)
    float x = normal.x();
    float y = normal.y();
    float z = normal.z();
    Eigen::Vector3f t = {x * y / sqrt(x * x + z * z), sqrt(x * x + z * z), z * y / sqrt(x * x + z * z)};
    Eigen::Vector3f b = normal.cross(t);
    Eigen::Matrix3f TBN;
    TBN << t, b, normal;
    float u = payload.tex_coords.x();
    float v = payload.tex_coords.y();
    float w = payload.texture->width;
    float h = payload.texture->height;
    auto dU = kh * kn * (payload.texture->getColor(u + 1.0f/w, v).norm() - payload.texture->getColor(u, v).norm());
    auto dV = kh * kn * (payload.texture->getColor(u, v + 1.0f/h).norm() - payload.texture->getColor(u, v).norm());
    Eigen::Vector3f ln;
    ln << -dU, -dV, 1;
    normal = (TBN * ln).normalized();


    Eigen::Vector3f result_color = {0, 0, 0};
    result_color = normal;

    return result_color * 255.f;
}
  • 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

displacement_fragment_shader

代码如下:

Eigen::Vector3f displacement_fragment_shader(const fragment_shader_payload& payload)
{
    
    Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
    Eigen::Vector3f kd = payload.color;
    Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

    auto l1 = light{{20, 20, 20}, {500, 500, 500}};
    auto l2 = light{{-20, 20, 0}, {500, 500, 500}};

    std::vector<light> lights = {l1, l2};
    Eigen::Vector3f amb_light_intensity{10, 10, 10};
    Eigen::Vector3f eye_pos{0, 0, 10};

    float p = 150;

    Eigen::Vector3f color = payload.color; 
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;

    float kh = 0.2, kn = 0.1;
    
    // TODO: Implement displacement mapping here
    // Let n = normal = (x, y, z)
    // Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
    // Vector b = n cross product t
    // Matrix TBN = [t b n]
    // dU = kh * kn * (h(u+1/w,v)-h(u,v))
    // dV = kh * kn * (h(u,v+1/h)-h(u,v))
    // Vector ln = (-dU, -dV, 1)
    // Position p = p + kn * n * h(u,v)
    // Normal n = normalize(TBN * ln)
    float x = normal.x();
    float y = normal.y();
    float z = normal.z();
    Eigen::Vector3f t = { x * y / sqrt(x * x + z * z), -sqrt(x * x + z * z), z * y / sqrt(x * x + z * z) };
    Eigen::Vector3f b = normal.cross(t);
    Eigen::Matrix3f TBN;
    TBN << t, b, normal;
    float u = payload.tex_coords.x();
    float v = payload.tex_coords.y();
    float w = payload.texture->width;
    float h = payload.texture->height;
    auto dU = kh * kn * (payload.texture->getColor(u + 1.0f / w, v).norm() - payload.texture->getColor(u, v).norm());
    auto dV = kh * kn * (payload.texture->getColor(u, v + 1.0f / h).norm() - payload.texture->getColor(u, v).norm());
    Eigen::Vector3f ln;
    ln << -dU, -dV, 1;
    
    point += (kn * normal * payload.texture->getColor(u, v).norm());
    normal = (TBN * ln).normalized();
    Eigen::Vector3f result_color = {0, 0, 0};
    result_color += ka.cwiseProduct(amb_light_intensity);
    for (auto& light : lights)
    {
        // TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular* 
        // components are. Then, accumulate that result on the *result_color* object.
        auto l = (light.position - point).normalized();
        auto r2 = (light.position - point).dot(light.position - point);
        auto v = (eye_pos - point).normalized();
        auto h = (v + l).normalized();

        auto Ls = ks.cwiseProduct(light.intensity / (r2)) * std::pow(std::max(0.0f, normal.dot(h)), p);
        auto Ld = kd.cwiseProduct(light.intensity / (r2)) * std::max(0.0f, normal.dot(l));

        result_color = result_color + Ls + Ld;

    }

    return result_color * 255.f;
}
  • 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

结果
在这里插入图片描述

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
  

闽ICP备14008679号