OpenGL 编程入门

OpenGL 是一个著名的标准,用于生成强大的、功能多样的 3D 以及 2D 图形。OpenGL 由 OpenGL 架构审查委员会 (ARB) 定义和发布。

本文是对 OpenGL 的入门介绍,旨在帮助您理解如何使用 OpenGL 进行绘图。

在撰写本文时,OpenGL 的最新版本是 4.4,它使用的绘图技术与本文介绍的不同。尽管如此,本文的目的是帮助您理解 OpenGL 的理念,而不是教您如何使用最新的 OpenGL 版本进行编码。因此,本文提供的源代码可以在安装了较旧 OpenGL 版本的 Linux 机器上运行。

安装 OpenGL

如果您在 Debian 7 系统上运行以下命令来查找所有包含 “opengl” 单词的软件包,您将获得大量输出(图 1)


$ apt-cache search opengl

图 1. 运行 apt-cache search opengl

Linux 有许多免费的 OpenGL 实现,但您只需要一个。我安装了 FreeGLUT,因为它最新。FreeGLUT 是 OpenGL Utility Toolkit (GLUT) 库的开源替代品。


root@mail:~# apt-get install freeglut3 freeglut3-dev libglew-dev
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following package was automatically installed 
 and is no longer required:
  fonts-liberation
Use 'apt-get autoremove' to remove it.
The following extra packages will be installed:
  libgl1-mesa-dev libglu1-mesa-dev libice-dev libpthread-stubs0
  libpthread-stubs0-dev libsm-dev libx11-dev libx11-doc 
  libxau-dev libxcb1-dev libxdmcp-dev libxext-dev libxt-dev 
  mesa-common-dev x11proto-core-dev x11proto-input-dev 
  x11proto-kb-dev x11proto-xext-dev xorg-sgml-doctools xtrans-dev
Suggested packages:
  libice-doc libsm-doc libxcb-doc libxext-doc libxt-doc
The following NEW packages will be installed:
  freeglut3 freeglut3-dev libgl1-mesa-dev libglu1-mesa-dev 
  libice-dev libpthread-stubs0 libpthread-stubs0-dev libsm-dev 
  libx11-dev libx11-doc libxau-dev libxcb1-dev libxdmcp-dev 
  libxext-dev libxt-dev mesa-common-dev x11proto-core-dev 
  x11proto-input-dev x11proto-kb-dev x11proto-xext-dev 
  xorg-sgml-doctools xtrans-dev
0 upgraded, 22 newly installed, 0 to remove and 0 not upgraded.
Need to get 7,651 kB of archives.
After this operation, 24.9 MB of additional disk space 
 will be used.
Do you want to continue [Y/n]?

您还需要一个 C++ 编译器来编译代码。

最后,您可能需要安装 mesa-utils 软件包,以便能够使用 glxinfo 命令。


# apt-get install mesa-utils

glxinfo 命令显示有关您的 OpenGL 安装的有用信息,您可以在以下输出中看到


...
GLX version: 1.4
GLX extensions:
    GLX_ARB_get_proc_address, GLX_ARB_multisample, 
    GLX_EXT_import_context, GLX_EXT_texture_from_pixmap, 
    GLX_EXT_visual_info, GLX_EXT_visual_rating,
    GLX_MESA_copy_sub_buffer, GLX_MESA_multithread_makecurrent,
    GLX_OML_swap_method, GLX_SGIS_multisample, GLX_SGIX_fbconfig,
    GLX_SGIX_pbuffer, GLX_SGI_make_current_read
OpenGL vendor string: VMware, Inc.
OpenGL renderer string: Gallium 0.4 on llvmpipe 
  (LLVM 3.4, 128 bits)
OpenGL version string: 2.1 Mesa 10.1.3
OpenGL shading language version string: 1.30
OpenGL extensions:
...

Mesa 是一个 3D 图形库,其 API 与 OpenGL 的 API 非常相似,几乎无法区分。

OpenGL 管线

图 2—取自《OpenGL Shading Language》一书(又名“橙皮书”)—展示了带有顶点和片段处理器的可编程 OpenGL 管线。正如您可以想象的那样,OpenGL 管线很复杂,但您不必完全理解它也能使用 OpenGL。管线展示了 OpenGL 在后台如何运作。较新版本的 OpenGL 管线甚至更加复杂!

图 2. OpenGL 架构

OpenGL 是一个大型状态机。大多数对 OpenGL 函数的调用都会修改您无法直接访问的全局状态。

旨在 OpenGL 可编程处理器之一上执行的 OpenGL Shading Language 代码称为着色器 (Shader)。OpenGL Shading Language 源于 C 语言(本文的范围不包括介绍 OpenGL Shading Language)。

OpenGL 没有定义窗口层,因为它试图保持平台中立,并将此功能留给操作系统。操作系统必须提供一个接受命令的渲染上下文,以及一个保存绘图命令结果的帧缓冲区。

矩阵代数广泛应用于 3D 图形中,因此了解如何加、乘、减和除矩阵对您有好处,尽管您不需要自己编写此类操作的代码。熟悉 3D 坐标并能够在纸上草绘您尝试绘制的 3D 场景也很有用。

绘制三角形

现在是时候编写一些真正的 OpenGL 代码了。列表 1 中的代码在执行时,会使用 OpenGL 在屏幕上绘制一个三角形。

列表 1. triangle.cc

// Programmer: Mihalis Tsoukalos
// Date: Wednesday 04 June 2014
//
// A simple OpenGL program that draws a triangle.

#include "GL/freeglut.h"
#include "GL/gl.h"

void drawTriangle()
{
    glClearColor(0.4, 0.4, 0.4, 0.4);
    glClear(GL_COLOR_BUFFER_BIT);

    glColor3f(1.0, 1.0, 1.0);
    glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);

        glBegin(GL_TRIANGLES);
                glVertex3f(-0.7, 0.7, 0);
                glVertex3f(0.7, 0.7, 0);
                glVertex3f(0, -1, 0);
        glEnd();

    glFlush();
}

int main(int argc, char **argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_SINGLE);
    glutInitWindowSize(500, 500);
    glutInitWindowPosition(100, 100);
    glutCreateWindow("OpenGL - Creating a triangle");
    glutDisplayFunc(drawTriangle);
    glutMainLoop();
    return 0;
}

列表 1 中用于设置 OpenGL 的代码量很大,但您只需要学习一次。

在 Debian 7 系统上,以下命令编译了 triangle.cc OpenGL 程序,没有任何错误消息


$ g++ triangle.cc -lglut -o triangle

在 Ubuntu Linux 系统上,相同的命令产生了以下错误消息


/usr/bin/ld: /tmp/ccnBP5li.o: undefined reference to symbol 
 ↪'glOrtho'
//usr/lib/x86_64-linux-gnu/mesa/libGL.so.1: error adding 
//symbols: DSO missing from command line
collect2: error: ld returned 1 exit status

解决方案是通过将可执行文件链接到额外的库 (-lGL) 来编译 triangle.cc 程序


mtsouk@mtsouk-VirtualBox:/media/sf_OpenGL.LJ/code$ g++ 
 ↪triangle.cc -lglut -lGL -o triangle

libGL.so 库接受 OpenGL 命令,并确保它们以某种方式显示在屏幕上。如果您的显卡没有 3D 加速,libGL 包含一个软件渲染器,它将 2D 图像作为输出提供给 X Window System。Mesa 就是这种情况。如果存在 GLX 扩展,libGL 也可以将 OpenGL 信息传递给 X Window System。然后,X Window System 可以借助 Mesa 进行软件渲染,或者使用硬件加速。

可执行文件的输出将生成图 3 中所示的三角形。triangle.cc 的正确编译证明您的 Linux 系统可以用于开发 OpenGL 应用程序。

图 3. 使用 OpenGL 绘制三角形

使用 OpenGL 绘制三角形的方法不止一种,主要取决于您使用的 OpenGL 版本。虽然本文介绍的方法是一种较旧的 OpenGL 应用程序编程方式,但我发现它简单易懂。请记住,无论您使用哪种方法,三角形的坐标都将是相同的。

注意:请记住,本文最重要的部分是代码!

使用 OpenGL 绘制立方体

接下来,让我们编写一个使用 OpenGL 绘制立方体的应用程序。您需要使用三角形来构建立方体。一个立方体有六个面,每个面至少需要定义两个三角形。我说“至少”的原因是,一般来说,如果您想获得更平滑、更精确的形状,可以使用更多三角形,但在绘制立方体时,这是不必要的。我相信您已经意识到,您总共需要 12 个三角形。

一个立方体也有八个顶点。每个三角形都需要定义三个不同的顶点。

列表 2 显示了 cube.cc 文件的完整源代码。

列表 2. cube.cc

// Programmer: Mihalis Tsoukalos
// Date: Wednesday 04 June 2014
//
// A simple OpenGL program that draws a colorful cube
// that rotates as you move the arrow keys!
//
// g++ cube.cc -lm -lglut -lGL -lGLU -o cube

#define GL_GLEXT_PROTOTYPES
#ifdef __APPLE__
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif
#include <math.h>

// Rotate X
double rX=0;
// Rotate Y
double rY=0;

// The coordinates for the vertices of the cube
double x = 0.6;
double y = 0.6;
double z = 0.6;

void drawCube()
{
        // Set Background Color
    glClearColor(0.4, 0.4, 0.4, 1.0);
        // Clear screen
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Reset transformations
    glLoadIdentity();

    // Rotate when user changes rX and rY
    glRotatef( rX, 1.0, 0.0, 0.0 );
    glRotatef( rY, 0.0, 1.0, 0.0 );

    // BACK
        glBegin(GL_TRIANGLES);
            glColor3f(0.4, 0.3, 0.5);
                glVertex3f(x, y, z);
                glVertex3f(x, -y, z);
                glVertex3f(-x, y, z);
        glEnd();

        glBegin(GL_TRIANGLES);
            glColor3f(0.5, 0.3, 0.2);
                glVertex3f(-x, -y, z);
                glVertex3f(x, -y, z);
                glVertex3f(-x, y, z);
        glEnd();

        // FRONT
        // Using 4 trianges!
        glBegin(GL_TRIANGLES);
            glColor3f(0.1, 0.5, 0.3);
                glVertex3f(-x, y, -z);
                glVertex3f(0, 0, -z);
                glVertex3f(-x, -y, -z);
        glEnd();

        glBegin(GL_TRIANGLES);
                glColor3f(0.0, 0.5, 0.0);
                glVertex3f(-x, -y, -z);
                glVertex3f(0, 0, -z);
                glVertex3f(x, -y, -z);
        glEnd();

        glBegin(GL_TRIANGLES);
            glColor3f(0.1, 0.3, 0.3);
                glVertex3f(-x, y, -z);
                glVertex3f(x, y, -z);
                glVertex3f(0, 0, -z);
        glEnd();

        glBegin(GL_TRIANGLES);
                glColor3f(0.2, 0.2, 0.2);
                glVertex3f(0, 0, -z);
                glVertex3f(x, y, -z);
                glVertex3f(x, -y, -z);
        glEnd();

        // LEFT
        glBegin(GL_TRIANGLES);
        glColor3f(0.3, 0.5, 0.6);
                glVertex3f(-x, -y, -z);
                glVertex3f(-x, -y, z);
                glVertex3f(-x, y, -z);
        glEnd();

        glBegin(GL_TRIANGLES);
                glColor3f(0.5, 0.5, 0.5);
                glVertex3f(-x, y, z);
                glVertex3f(-x, -y, z);
                glVertex3f(-x, y, -z);
        glEnd();

        // RIGHT
        glBegin(GL_TRIANGLES);
        glColor3f(0.2, 0.2, 0.2);
                glVertex3f(x, y, z);
                glVertex3f(x, y, -z);
                glVertex3f(x, -y, z);
        glEnd();

        glBegin(GL_TRIANGLES);
        glColor3f(0.0, 0.0, 0.0);
                glVertex3f(x, -y, -z);
                glVertex3f(x, y, -z);
                glVertex3f(x, -y, z);
        glEnd();

        // TOP
        glBegin(GL_TRIANGLES);
        glColor3f(0.6, 0.0, 0.0);
                glVertex3f(x, y, z);
                glVertex3f(x, y, -z);
                glVertex3f(-x, y, -z);
        glEnd();

        glBegin(GL_TRIANGLES);
        glColor3f(0.6, 0.1, 0.2);
                glVertex3f(-x, y, z);
                glVertex3f(x, y, z);
                glVertex3f(-x, y, -z);
        glEnd();

        // BOTTOM
        glBegin(GL_TRIANGLES);
        glColor3f(0.4, 0.0, 0.4);
                glVertex3f(-x, -y, -z);
                glVertex3f(-x, -y, z);
                glVertex3f(x, -y, z);
        glEnd();

        glBegin(GL_TRIANGLES);
                glColor3f(0.3, 0.0, 0.3);
                glVertex3f(x, -y, -z);
                glVertex3f(-x, -y, -z);
                glVertex3f(x, -y, z);
        glEnd();

    glFlush();
    glutSwapBuffers();
}

void keyboard(int key, int x, int y)
{
    if (key == GLUT_KEY_RIGHT)
        {
                rY += 15;
        }
    else if (key == GLUT_KEY_LEFT)
        {
                rY -= 15;
        }
    else if (key == GLUT_KEY_DOWN)
        {
                rX -= 15;
        }
    else if (key == GLUT_KEY_UP)
        {
                rX += 15;
        }

    // Request display update
    glutPostRedisplay();
}


int main(int argc, char **argv)
{
        // Initialize GLUT and process user parameters
        glutInit(&argc, argv);

        // Request double buffered true color window with Z-buffer
        glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);

        glutInitWindowSize(700,700);
        glutInitWindowPosition(100, 100);

        // Create window
        glutCreateWindow("Linux Journal OpenGL Cube");

        // Enable Z-buffer depth test
        glEnable(GL_DEPTH_TEST);

        // Callback functions
        glutDisplayFunc(drawCube);
        glutSpecialFunc(keyboard);

        // Pass control to GLUT for events
        glutMainLoop();

        return 0;
}

图 4 显示了 cube.cc 应用程序的两个不同屏幕截图。该程序的左侧屏幕截图仅显示一个正方形,因为您正对着立方体的正面,看不到立方体的其他五个面。因此,您在屏幕上看到的形状看起来像是 2D 的。当您开始使用箭头键旋转立方体时,您可以看出它是一个 3D 形状。

图 4. cube.cc 的图形输出

代码解释

每个三角形由三个顶点定义。每个顶点借助单个点 (x,y,z) 定义。每个点都带有三个数字(坐标),因为 OpenGL 使用 3D 空间。一个立方体需要定义八个顶点。

作为练习,立方体的正面使用四个三角形制成。图 5 显示了使用四个三角形定义立方体正面时的坐标。点 (0,0,-0.6) 是任意选择的。您只需要一个属于立方体正面的点。

图 5. 立方体正面的坐标

定义顶点

图 6 显示了 x=0.6、y=0.6 和 z=0.6 时立方体顶点的坐标。请注意,定义边的顶点,其三个坐标中有两个坐标完全相同。

图 6. 立方体的顶点

如图 6 所示,定义正面的四个顶点的坐标和定义背面的四个顶点的相应坐标仅在 z 轴坐标的值上有所不同。

定义三角形

三角形基于顶点。每个三角形都需要定义三个顶点。定义立方体的每个面都需要两个三角形,除了正面使用了四个三角形。以下命令基于 x、y 和 z 的值创建一个彩色三角形


glBegin(GL_TRIANGLES);
glColor3f(0.4, 0.0, 0.4);
        glVertex3f(-x, -y, -z);
        glVertex3f(-x, -y, z);
        glVertex3f(x, -y, z);
glEnd();

更改颜色

您可以使用 glColor3f(...) 命令更改形状的颜色。glColor3f(...) 命令接受三个参数,这些参数表示所需颜色的 RGB 值。

更改视角

您可以使用以下命令更改场景的视角


glRotatef(rX, 1.0, 0.0, 0.0);
glRotatef(rY, 0.0, 1.0, 0.0);

当用户按下四个箭头键之一时,视角会相应地改变。

绘制立方体

绘制立方体是逐面进行的。除了正面使用了四个三角形外,每个面都需要绘制两个三角形。

一旦您正确获得了三角形的坐标,绘制就非常容易了。每个三角形的绘制都以 glBegin(GL_TRIANGLES) 命令开始,以 glEnd() 命令结束。GL_TRIANGLES 是一个 OpenGL 图元。其他图元类型包括 GL_POINTSGL_LINESGL_LINE_STRIPGL_LINE_LOOPGL_TRIANGLE_STRIPGL_TRIANGLE_FANGL_QUADSGL_QUAD_STRIPGL_POLYGON。最终,OpenGL 中的每个图元形状都由一个或多个三角形表示。

如果您使用三角形 (GL_TRIANGLES) 绘制,则顶点放置的顺序并不重要。如果您使用矩形 (GL_POLYGON) 绘制,则矩形的四个顶点必须按正确的顺序绘制,尽管顺时针 (CW) 或逆时针 (CCW) 绘制都没关系。如果绘制顺序错误,您尝试绘制的矩形将有一个大洞。

使用箭头键

使用箭头键非常简单。以下代码检查箭头键并做出相应的操作


void keyboard(int key, int x, int y)
{
    if (key == GLUT_KEY_RIGHT)
        {
                rY += 15;
        }
    else if (key == GLUT_KEY_LEFT)
        {
                rY -= 15;
        }
    else if (key == GLUT_KEY_DOWN)
        {
                rX -= 15;
        }
    else if (key == GLUT_KEY_UP)
        {
                rX += 15;
        }

    // Request display update
    glutPostRedisplay();
}

keyboard(...) 函数在 main(...) 函数中使用以下代码行注册


glutSpecialFunc(keyboard);
自动旋转立方体

作为奖励,让我们看看如何使立方体自动旋转(图 7)。

这次,立方体使用矩形绘制。由于立方体有六个面,因此只需要六个矩形。使用矩形绘制更容易,需要的代码更少,但当使用三角形时,复杂的 OpenGL 代码运行速度更快。

注意:每个对象都可以拆分为三角形,但三角形不能拆分为三角形以外的任何东西。

图 7. rotateCube.cc 的输出

列表 3 显示了 rotateCube.cc 的源代码。

列表 3. rotateCube.cc

// Programmer: Mihalis Tsoukalos
// Date: Wednesday 04 June 2014
//
// A simple OpenGL program that draws a triangle
// and automatically rotates it.
//
// g++ rotateCube.cc -lm -lglut -lGL -lGLU -o rotateCube

#include <iostream>
#include <stdlib.h>

// the GLUT and OpenGL libraries have to be linked correctly
#ifdef __APPLE__
#include <OpenGL/OpenGL.h>
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif

using namespace std;

// The coordinates for the vertices of the cube
double x = 0.6;
double y = 0.6;
double z = 0.6;

float angle = 0.0;

void drawCube()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Reset transformations
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();

        glTranslatef(0.0, 0.0, -5.0);

        // Add an ambient light
        GLfloat ambientColor[] = {0.2, 0.2, 0.2, 1.0};
        glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambientColor);

        // Add a positioned light
        GLfloat lightColor0[] = {0.5, 0.5, 0.5, 1.0};
        GLfloat lightPos0[] = {4.0, 0.0, 8.0, 1.0};
        glLightfv(GL_LIGHT0, GL_DIFFUSE, lightColor0);
        glLightfv(GL_LIGHT0, GL_POSITION, lightPos0);

        glTranslatef(0.5, 1.0, 0.0);
        glRotatef(angle, 1.0, 1.0, 1.0);

    glRotatef( angle, 1.0, 0.0, 1.0 );
    glRotatef( angle, 0.0, 1.0, 1.0 );
        glTranslatef(-0.5, -1.0, 0.0);

        // Create the 3D cube

    // BACK
    glBegin(GL_POLYGON);
    glColor3f(0.5, 0.3, 0.2);
    glVertex3f(x, -y, z);
    glVertex3f(x, y, z);
    glVertex3f(-x, y, z);
    glVertex3f(-x, -y, z);
    glEnd();

        // FRONT
        glBegin(GL_POLYGON);
        glColor3f(0.0, 0.5, 0.0);
        glVertex3f(-x, y, -z);
        glVertex3f(-x, -y, -z);
        glVertex3f(x, -y, -z);
        glVertex3f(x, y, -z);
        glEnd();

        // LEFT
        glBegin(GL_POLYGON);
        glColor3f(0.5, 0.5, 0.5);
        glVertex3f(-x, -y, -z);
        glVertex3f(-x, -y, z);
        glVertex3f(-x, y, z);
        glVertex3f(-x, y, -z);
        glEnd();


        // RIGHT
        glBegin(GL_POLYGON);
        glColor3f(0.0, 0.0, 0.0);
        glVertex3f(x, -y, -z);
        glVertex3f(x, -y, z);
        glVertex3f(x, y, z);
        glVertex3f(x, y, -z);
        glEnd();

        // TOP
        glBegin(GL_POLYGON);
        glColor3f(0.6, 0.0, 0.0);
        glVertex3f(x, y, z);
        glVertex3f(-x, y, z);
        glVertex3f(-x, y, -z);
        glVertex3f(x, y, -z);
        glEnd();


        // BOTTOM
        glBegin(GL_POLYGON);
        glColor3f(0.3, 0.0, 0.3);
        glVertex3f(-x, -y, -z);
        glVertex3f(-x, -y, z);
        glVertex3f(x, -y, z);
        glVertex3f(x, -y, -z);
        glEnd();

        glFlush();
    glutSwapBuffers();
}

// Function for increasing the angle variable smoothly, 
// keeps it <=360
// It can also be implemented using the modulo operator.
void update(int value)
{
        angle += 1.0f;
        if (angle > 360)
                {
                        angle -= 360;
        }

        glutPostRedisplay();
        glutTimerFunc(25, update, 0);
}

// Initializes 3D rendering
void initRendering()
{
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_COLOR_MATERIAL);

        // Set the color of the background
        glClearColor(0.7f, 0.8f, 1.0f, 1.0f);
        glEnable(GL_LIGHTING);
        glEnable(GL_LIGHT0);
        glEnable(GL_NORMALIZE);
}


// Called when the window is resized
void handleResize(int w, int h)
{
        glViewport(0, 0, w, h);
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        gluPerspective(45.0, (double)w / (double)h, 1.0, 200.0);
}


int main(int argc, char **argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
    glutInitWindowSize(700, 700);
    glutInitWindowPosition(100, 100);
    glutCreateWindow("OpenGL - Rotating a Cube");
        initRendering();

    glutDisplayFunc(drawCube);
        glutReshapeFunc(handleResize);

        // Add a timer for the update(...) function
    glutTimerFunc(25, update, 0);

    glutMainLoop();
    return 0;
}

请注意,triangle.cc、cube.cc 和 rotateCube.cc 的 main(...) 函数非常相似,尽管这三个程序执行不同的任务。

这里的关键是 glutTimerFunc(...) 函数的用法。它为 update(...) 函数注册一个定时器回调,该回调将在指定的毫秒数后触发。update(...) 函数每次被调用时都会更改场景的角度。

结论

OpenGL 编程并不容易,但本文应该可以帮助您快速开始编写 OpenGL 应用程序。如果您想精通 OpenGL,请坚持练习编写更多的 OpenGL 程序。OpenGL 入门容易,但精通难。

致谢

我要感谢 Nikos Platis 博士与我分享了他的一小部分 OpenGL 知识。

资源

OpenGL: http://www.opengl.org

学习现代 3D 图形编程: http://www.arcsynthesis.org/gltut

OpenGL 超级宝典,第 6 版,Graham Sellers, Richard S. Wright 和 Nicholas Haemel 著,Addison Wesley 出版,ISBN: 0321902947

GLEW: http://glew.sourceforge.net

Mesa 3D 图形库: http://www.mesa3d.org

Mihalis Tsoukalos 是一位 UNIX 管理员和开发人员,一位 DBA 和数学家,喜欢技术写作。他是 Go 系统编程精通 Go 的作者。您可以在 http://www.mtsoukalos.eu 和 @mactsouk 联系到他。

加载 Disqus 评论