前些日子在YouTube上看到一个模仿蚁群行为的视频,感觉十分炫酷。视频中提到进行大尺寸模拟时,通过使用着色器(Shader)在显卡上并行计算,能够有效提高计算速度。所以我也想复刻一个,当然还是用Java/Kotlin,其他语言半斤八两的不是很熟。于是为了达成这个目的,开始了OpenGL的学习,本文作为学习OpenGL的Hello World,尽我所能的利用GLFW在窗口中画一个(椭)圆。
前言
一般而言我是不喜欢和GUI打交道的,因为处理各种界面的东西实在是太繁琐了。截至目前,能够直接调用GPU的框架不多,无外乎就是OpenGL、DirectX、Vulkan,而这三者对比之下,OpenGL或许更对我口味一些。由于我在C/C++方面学艺不精,虽然C语言期末上机拿了个满分,但那只是基础的编程题,真要是用到一些底层的东西,还是蛮困难的。尤其考虑在Windows上开发C(不使用WSL的话)还得搞Visual Studio的编译器,并且在微软的魔改下C代码通常不具有可移植性。虽然这个项目只是做着玩玩,但我并不希望它和某一个具体的平台实现耦合在一起,那样很不优雅,而Vulkan实在是太底层了,最终我选择OpenGL,它在JVM平台上的绑定有两个,一个是JOGL,另一个是LWJGL,我更倾向后者一些,因为后者的官网清晰明了。
在配置方面,我选择的Minimal OpenGL,实际上的话只引用OpenGL就可以了,因为不是做游戏,所以OpenAL什么的完全可以不必理会。
画个圆
由于OpenGL基于C/C++实现,所以也继承了C系列语言的惯例,往往做一个事情有很多种做法,在一开始的尝试过程中我达到了一种每次运行必把JVM搞崩的境界,后来又查了查教程,发现只是画个圆竟然还有好多种做法。这里参考LWJGL的官方教程,使用了一种据说已经被废弃的技术1,名叫fixed pipeline
,在一个窗口上画一个圆/椭圆——取决于窗口的形状。
窗口
在画圆之前,至少得先有个窗口,类似于Java AWT中的Canvas,要想画点什么,至少得现有一个画布,在OpenGL中这个画布就是窗口。在OpenGL中维护一个窗口大概有如下三个步骤:初始化——循环——释放资源,初始化阶段将初始化OpenGL库,并在该阶段对窗体做出一些定义(诸如能否改变大小,是否可见等),成功创建窗体后进入一个循环,在这个循环内程序将接收用户对窗a体的输入,并对此做出响应,同时渲染出下一帧画面;而一旦用户提出想要退出程序,程序将会打破循环,进入到收尾阶段,这个阶段程序需要结束窗口并释放窗口占用的资源,如果必要的话还要在退出之前告诉OpenGL将占用的资源归还给操作系统。其中有关资源管理的部分最为繁琐,尤其是JVM中OpenGL是作为外部代码,通过JNI调用的,有时候在Java中看明明是几行之内的代码,可是对于C语言来说这也许就是两个线程之间的代码,如果不做好资源规划,很有可能会因为访问了权限外的内存地址,进而导致JVM崩溃。
参考LWJGL的官方教程,创建一个窗口的代码如下:
package info.skyblond.antSimu
import info.skyblond.antSimu.utils.opengl.glfmCreateSlf4jLogger
import org.lwjgl.BufferUtils
import org.lwjgl.Version
import org.lwjgl.glfw.Callbacks.glfwFreeCallbacks
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL11.*
import org.lwjgl.system.MemoryUtil.NULL
import org.slf4j.LoggerFactory
import kotlin.math.*
fun main() {
val logger = LoggerFactory.getLogger("Application")
println("App start with OpenGL version ${Version.getVersion()}")
// ------------------------------------------------------------
// init external openGL
// ------------------------------------------------------------
// GLFW stands for Graphic Library FrameWork
// First set a error callback, the default implementation
glfmCreateSlf4jLogger(logger).set()
// do init things
require(glfwInit()) { "Unable to initialize GLFW" }
// set window hint to default
// window hint is needed for window creation
glfwDefaultWindowHints()
// hide window after creation
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
// disable resize on window
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE)
// create window
val windowHandler: Long = glfwCreateWindow(
1024, 1024,
"Hello world!",
NULL, NULL // windowed mode and don't share resource with others
)
// check windowHandler to ensure window is created
require(windowHandler != NULL) { "Failed to create the GLFW window" }
// set a key handler for key events
// scan code is ignored since it only used for unknown keys
// mods is used to identify shift, ctrl, alt .etc
glfwSetKeyCallback(windowHandler) { window, key, _, action, _ ->
logger.info("Logged key event: key: $key, action: $action")
// close window if esc if pressed and released
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, true)
// this should close signal will handled in rendering loop
}
// Get the resolution of the main monitor
val videoMode = glfwGetVideoMode(glfwGetPrimaryMonitor())!!
val pWidth = BufferUtils.createIntBuffer(1)
val pHeight = BufferUtils.createIntBuffer(1)
// Get the window size passed to glfwCreateWindow
glfwGetWindowSize(windowHandler, pWidth, pHeight)
// Center the window
glfwSetWindowPos(
windowHandler,
(videoMode.width() - pWidth[0]) / 2,
(videoMode.height() - pHeight[0]) / 2
)
// Make window context as current context
glfwMakeContextCurrent(windowHandler)
// Enable V-sync
glfwSwapInterval(1)
// let window visible
glfwShowWindow(windowHandler)
// ------------------------------------------------------------
// loop for updating window
// ------------------------------------------------------------
// This line is critical for LWJGL's interoperation with GLFW's
// OpenGL context, or any context that is managed externally.
// LWJGL detects the context that is current in the current thread,
// creates the GLCapabilities instance and makes the OpenGL
// bindings available for use.
// i.e. to let lwjgl can use our windows handler, we had to call this
// to make sure lwjgl create proper capabilities for current context
GL.createCapabilities()
// specifies the red, green, blue, and alpha values used by glClear to clear the color buffers.
// in this case we set it to a beautiful color
glClearColor(0.2f, 0.3f, 0.3f, 1.0f)
logger.info("Start rendering loop")
// the actual render loop
while (!glfwWindowShouldClose(windowHandler)) {
// poll window event, key handlers will only be called during this time
glfwPollEvents()
// clear color buffer
glClear(GL_COLOR_BUFFER_BIT)
// start drawing frame
// TODO RENDER
//swap color buffer, means that next frame is done
glfwSwapBuffers(windowHandler)
}
logger.info("Close signal received, termination")
// clean up and terminate window
glfwFreeCallbacks(windowHandler)
glfwDestroyWindow(windowHandler)
// Terminate GLFW and free the error callback
glfwTerminate()
// set null for error callback
// and free previous one
glfwSetErrorCallback(null)?.free()
}
下面我们逐行解释代码:
初始化
首先我们定义了一个错误处理的回调函数,这个函数的作用是把OpenGL产生的错误重定向到Logger中。这个是我自己实现的,如果使用System.err
或者其他自定义流的话,可以使用自带的GLFWErrorCallback.createPrint(System.err).set()
来代替,本质上我的实现与自带的无异,只是把错误信息打印到Logger中罢了:
package info.skyblond.antSimu.utils.opengl
import org.lwjgl.glfw.GLFW
import org.lwjgl.glfw.GLFWErrorCallback
import org.lwjgl.system.APIUtil
import org.slf4j.Logger
/**
* Create a [GLFWErrorCallback] instance which print error
* into given logger. Same as [GLFWErrorCallback.createPrint] but
* rather than writing into output stream, it print through logger.
* */
fun glfmCreateSlf4jLogger(logger: Logger): GLFWErrorCallback {
return object : GLFWErrorCallback() {
// Map GLFM error code to Description
// according to https://www.glfw.org/docs/latest/group__errors.html
// GLFM error code range from 0x10001 to 0x1000A
// but lwjgl demo use range 0x10001 to 0x1ffff
private val ERROR_CODES = APIUtil.apiClassTokens(
{ _, value: Int -> value in 0x10001..0x1ffff }, null,
GLFW::class.java
)
// print message to logger when error occur
override fun invoke(error: Int, description: Long) {
val msg = getDescription(description)
val stringBuilder = StringBuilder()
// parse error code
stringBuilder.append(String.format("LWJGL error %d: %s\n", error, ERROR_CODES[error]))
// print msg
stringBuilder.append("\tDescription : $msg\n")
// print stack
stringBuilder.append("\tStacktrace :\n")
val stack = Thread.currentThread().stackTrace
for (i in 4 until stack.size) {
stringBuilder.append("\t\t")
stringBuilder.append(stack[i].toString() + "\n")
}
logger.error(stringBuilder.toString())
}
}
}
在确保错误回调函数设置妥当之后,便可以开始OpenGL相关库的初始化工作了,如果期间出错,也会通过刚刚设置的回调函数在日志中打印出错误信息:
require(glfwInit()) { "Unable to initialize GLFW" }
如果成功初始化,那么接下来就可以使用OpenGL提供的诸多功能了,这里使用GLFW创建窗体,顺带一提,GLFW的全称是Graphics Library FrameWork,这缩写真是深得C的精髓。在创建窗体之前,我们先要规定窗体的一些具体属性,比如创建的窗体一开始是否可见,能否修改大小之类的:
// set window hint to default
// window hint is needed for window creation
glfwDefaultWindowHints()
// hide window after creation
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
// disable resize on window
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE)
以上代码,首先将各种设置回复成默认设置,然后在创建窗口的时候先隐藏,因为窗体被创建的时候,位置由操作系统决定,我们想要让创建的窗体处于屏幕正中,因此需要先隐藏,然后手动调整位置之后在显示出来。最后为了保证我们画的圆不会变形,要求窗体不能够改变大小(作为教程,鼓励各位读者将上面的GLFW_FALSE
改为GLFW_TRUE
,看看改变窗口大小会对绘制的图片产生什么样的影响)。
设定好上面的各种Hint之后,我们就可以真正的创建窗口了:
// create window
val windowHandler: Long = glfwCreateWindow(
1024, 1024,
"Hello world!",
NULL, NULL // windowed mode and don't share resource with others
)
// check windowHandler to ensure window is created
require(windowHandler != NULL) { "Failed to create the GLFW window" }
由于创建窗口可能失败,所以调用创建窗口的函数之后要检查窗口句柄不为空(Java里的null对应这个框架里的0,这里NULL
是LWJGL提供的一个常量,值就是0),创建成功后即可对窗口进行操作了。值得一提的是前两个参数是窗口的宽度和高度,第三个参数是窗口的标题,第四个参数是使用哪一个屏幕,如果指定屏幕的话则会全屏占用这个屏幕,使用窗口模式则传入NULL,最后一个参数表示资源共享,这是为了方便多个窗口共享一个资源,节省内存。这里我们不与其他窗口共享资源,传入NULL即可。
创建窗口之后我们还需要对窗口进行一些设置,除了之前说的调整窗口位置,还要设置一系列的事件回调,对应Java UI中的监听器:
// set a key handler for key events
// scan code is ignored since it only used for unknown keys
// mods is used to identify shift, ctrl, alt .etc
glfwSetKeyCallback(windowHandler) { window, key, _, action, _ ->
logger.info("Logged key event: key: $key, action: $action")
// close window if esc if pressed and released
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, true)
// this should close signal will handled in rendering loop
}
这个函数将会为给定的窗口设定按键回调,当键盘有按键按下时将会调用这个函数,这个函数有五个参数,分别是事件发生的窗口句柄(这里不推荐用已有的windowHandler
做闭包使用,原因是Java的代码可能会与OpenGL的代码不在一个线程,当然这里都是主线程,但未来可能会在独立的线程中,这样乱访问内存会出问题)、按下的key编号、按下的键的扫描码(scan code,每个键在键盘上有唯一的scan code)、动作和模式;动作表示这个键的动作:按下、重复、释放;模式表征了shift、ctrl之类的状态。
这里我们设计当用户按下ESC的时候,立刻发出WindowsShouldClose
信号,表示窗口应该关闭了。这个信号将会在后面的渲染循环中处理。
接下来我们调整窗口的位置:
// Get the resolution of the main monitor
val videoMode = glfwGetVideoMode(glfwGetPrimaryMonitor())!!
val pWidth = BufferUtils.createIntBuffer(1)
val pHeight = BufferUtils.createIntBuffer(1)
// Get the window size passed to glfwCreateWindow
glfwGetWindowSize(windowHandler, pWidth, pHeight)
// Center the window
glfwSetWindowPos(
windowHandler,
(videoMode.width() - pWidth[0]) / 2,
(videoMode.height() - pHeight[0]) / 2
)
首先要获取主显示器的视频模式,可以理解为包含了分辨率的信息。随后获取当前窗体的大小,然后设置指定的窗口位置为屏幕正中间。
至此初始化工作全部设置妥当,做一些收尾工作便可将初始化的过程告一段落了:
// Make window context as current context
glfwMakeContextCurrent(windowHandler)
// Enable V-sync
glfwSwapInterval(1)
// let window visible
glfwShowWindow(windowHandler)
OpenGL的工作原理相当于一个巨大的状态机,因此要对某一个窗口进行操作(即后续需要在这个窗口中渲染),首先得把那个窗口的context(上下文)设置为当前的上下文,然后为了避免刷新太快导致画面撕裂,我们指定每次交换缓冲区之前必须等待屏幕刷新一次,这个东西在有几种经常叫作「垂直同步」,因为要等到屏幕在垂直方向扫描到同步区(参考以前的行扫描电视信号,或者VGA标准)的时候才会更换缓冲区,因此得名垂直同步。
一切妥当之后,告诉OpenGL显示窗口,初始化阶段至此完成。
循环渲染窗口
截至目前我们只是构造了一个空空如也的窗口。下面我们至少要把渲染的框架搭建出来,然后再聊画圆或者其他什么更高级的事情。
首先是一个LWJGL很关键的调用:
// This line is critical for LWJGL's interoperation with GLFW's
// OpenGL context, or any context that is managed externally.
// LWJGL detects the context that is current in the current thread,
// creates the GLCapabilities instance and makes the OpenGL
// bindings available for use.
// i.e. to let lwjgl can use our windows handler, we had to call this
// to make sure lwjgl create proper capabilities for current context
GL.createCapabilities()
这一行代码有好几行注释,足见其重要性。简单的翻译一下就是,为了让LWJGL框架知晓并能够管理OpenGL创建的窗口上下文(避免内存泄漏或访问异常),需要在当前线程中调用这个函数以让LWJGL感知到当前正在使用的上下文,这样就可以自动创建GLCapabilities
实例供后续OpenGL函数绑定使用。
之所以不把它放到初始化中,是因为这个调用需要在渲染线程伊始调用的,如果放在初始化中调用,很可能会造成主线程调用后没有对窗口更新,而需要更新窗口的渲染线程则因为没有调用该函数而出错。
之后还要为窗口设置一个底色,默认底色是纯黑色,这里我们换一个漂亮的颜色:
// specifies the red, green, blue, and alpha values used by glClear to clear the color buffers.
// in this case we set it to a beautiful color
glClearColor(0.2f, 0.3f, 0.3f, 1.0f)
这个颜色的格式是(Red, Green, Blue, Alpha)
。准备妥当之后就可以开始执行更新窗体的循环了:
logger.info("Start rendering loop")
// the actual render loop
while (!glfwWindowShouldClose(windowHandler)) {
// poll window event, key handlers will only be called during this time
glfwPollEvents()
// clear color buffer
glClear(GL_COLOR_BUFFER_BIT)
// start drawing frame
// TODO RENDER
//swap color buffer, means that next frame is done
glfwSwapBuffers(windowHandler)
}
这个循环的条件是!glfwWindowShouldClose(windowHandler)
,即只要没有收到退出信号,就一直执行更新。每次更新窗口前先收集窗口的输入事件,包括键盘事件、鼠标事件、窗口大小改变的事件等,之前设置的相应的回调函数也会在这行代码执行时被调用。
随后我们要清空当前的缓冲区。一般而言熟悉GUI的读者可能知道双缓冲这回事儿。双缓冲即一个窗口有两个缓冲区,一个缓冲区正在被显示在屏幕上,而另一个缓冲区正在被写入新的图像数据,完成后交换缓冲区,这样新写入的图像数据就被显示到屏幕上,而更新的数据则写入到刚刚换下来的缓冲区中。这样的作法是为了将显示和渲染分离开,一方面提高了程序的性能(不必等待屏幕显示完毕才能写入新的数据),另一方面则是避免画面撕裂(屏幕尚未显示,新的数据就已经覆盖了要显示的数据)。
清空缓冲区后就可以开始我们的渲染工作了。由于目前只是准备一个窗口的框架,因此这里有一个TODO,将在后文补上。
渲染完成后就是如上所说的,要交换缓冲区。至此本次渲染循环结束,如果窗口没有收到退出信号,那么程序将继续执行下一次渲染循环。
资源释放
虽然JVM在退出时会将全部资源归还操作系统,但是为了避免外部C代码导致内存泄漏,还是有必要手动进行资源回收的:
logger.info("Close signal received, termination")
// clean up and terminate window
glfwFreeCallbacks(windowHandler)
glfwDestroyWindow(windowHandler)
// Terminate GLFW and free the error callback
glfwTerminate()
// set null for error callback
// and free previous one
glfwSetErrorCallback(null)?.free()
首先是回收当前的窗口,先释放所有的回调函数指针,这样JVM在下一次垃圾回收的时候就可以释放对应的函数对象;然后摧毁窗口,这是保证OpenGL那边不会一直占用窗口的上下文。窗口关闭之后还要继续关闭OpenGL,最后释放我们一开始创建的出错回调程序。
至此OpenGL的调用全部结束,程序可以安全的退出或者执行其他任务,而不必担心外部C代码导致内存泄漏。
截至目前,如果运行上述代码的话,将会出现一个空白(有底色)的窗口,按一下ESC能够退出窗口。
画圆
现在有了窗体的架子,我们就可以开始画圆了。
坐标
与Java AWT或其他UI库不同,我们没法直接调用一个函数,告诉它圆心和半径,然后就万事大吉。在OpenGL的世界中,我们需要至少计算出圆上的坐标,然后交给OpenGL连成一条闭曲线(使用GL_LINE_LOOP
),或者让它切分成三角形变成一块封闭的面积(使用GL_TRIANGLE_FAN
)。无论使用哪种方法,都需要我们计算出圆上的坐标。
谈到坐标,OpenGL的坐标系统和Java里的坐标还不太一样。如果用过Java的UI库的话,绘制元素都是根据像素坐标来的,而OpenGL则使用标准化设备坐标(Normalized Device Coordinates),它的范围是[-1,1]
,其中-1
表示窗口的最左侧(用在x
上)或最下侧(用在y
上),而1
则表示窗口的最右侧(用在x
上)或最上侧(用在y
上),(0,0)
表示窗口的正中心,这与传统的根据像素定位的坐标系统不一样,在像素定位的坐标系统中(0,0)
往往表示窗口的左上角。
为了计算圆上的坐标,我们可以利用参数方程:
假设我们将圆切分成$n$段,则第$i$段对应的起始角度为:
$$ \theta_i = \frac{2 \pi i}{n} $$
由此我们可以通过三角函数计算出该角度对应的圆上的坐标$(x_i, y_i)$:
$$ \left\{ \begin{array}{ll} x_i = r \cdot \cos (\theta_i)\\ y_i = r \cdot \sin (\theta_i)\\ \end{array} \right. $$
当$n$足够大时,由于像素是离散的,因此通过这种方法画出来的多边形也就足够像圆了。
代码
这部分代码需要在负责更新的窗口中实现:
// radius of the circle
var radius = 0.75
// how fine the circle should be
var segments = 2000
// the actual render loop
while (!glfwWindowShouldClose(windowHandler)) {
// poll window event, key handlers will only be called during this time
glfwPollEvents()
// clear color buffer
glClear(GL_COLOR_BUFFER_BIT)
// start drawing frame
glColor3f(1.0f, 1.0f, 1.0f)
// set vertexes on the line to form a circle
glBegin(GL_TRIANGLE_FAN)
for (i in 0 until segments) {
val theta = 2 * PI * i / segments
val x = radius * cos(theta)
val y = radius * sin(theta)
glVertex2f(x.toFloat(), y.toFloat())
}
// end drawing
glEnd()
//swap color buffer, means that next frame is done
glfwSwapBuffers(windowHandler)
}
在清空颜色缓冲区之后(OpenGL中的缓冲区实际有好几个,每一对儿负责不同的工作,其中颜色缓冲区主要是负责存储颜色数据的),就可以开始绘制圆了。这里需要说明的是,OpenGL的工作原理除了是一个大的状态机,他在渲染的时候还是个流水线:程序员首先需要指定要画的基础图形和顶点(Vertex)信息,随后在顶点着色器中对顶点的坐标进行变换坐标,然后这些变换后的坐标信息和代码中指定的信息(这里我们使用GL_TRIANGLE_FAN
),将顶点组装成图元,对于显卡来说,它的图元就是三角形,也正是这一步,OpenGL将我们给的圆上的诸多坐标拼成许多个三角形,然后再通过片段着色器等一系列着色器的处理,最终形成一个十分近似圆的多边形,而后这个多边形在经过不同的着色器处理,最终呈现在窗口中。
在目前这个阶段,我们不需要实现任何着色器,上述着色器的默认行为已经在Fixed pipeline
中预先定义好了,我们通过glBegin(GL_TRIANGLE_FAN)
告诉OpenGL:我们接下来提供的点信息是用于构造TRIANGLE_FAN
的。在后面的循环中我们计算出每一个点的坐标,并通过glVertex2f
函数提交给OpenGL。当我们定义完定点之后就可以结束这个定义过程了:调用glEnd
。
但是画圆总得有个颜色吧,如果用片段着色器给圆上色,未免有些太复杂了,好在Fixed Pipeline
中也提供了默认的片段着色器:glColor3f
即可指定我们当前想要使用的颜色。如果在一次绘制过程中提供了多次glColor3f
,那么默认的着色器将会为我们加一个渐变效果。
上面的代码只是把圆涂成了白色,这样未免有些无聊,截图放在文章里也不够炫酷,不如我们把它改成彩色的:
for (i in 0 until segments) {
val theta = 2 * PI * i / segments
val x = radius * cos(theta)
val y = radius * sin(theta)
glColor3f(cos(theta).toFloat(), sin(theta).toFloat(), i / segments.toFloat())
glVertex2f(x.toFloat(), y.toFloat())
}
绘制过程中横坐标决定颜色的红色分量,纵坐标决定颜色的绿色分量,而段数则决定了蓝色分量:
有点炫酷。
至此我们就完成了画圆工作。但只是画圆的话,未免有些太过单调了?
交互式画圆
我决定在这个圆的基础上追加一些交互性,诸如调整圆的分段数量,半径等,利用键盘来改变这些参数:
// radius of the circle
var radius = 0.75
// how fine the circle should be
var segments = 20
// set a key handler for key events
// scan code is ignored since it only used for unknown keys
// mods is used to identify shift, ctrl, alt .etc
glfwSetKeyCallback(windowHandler) { window, key, _, action, _ ->
logger.info("Logged key event: key: $key, action: $action")
// close window if esc if pressed and released
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, true)
else if (key == GLFW_KEY_W && action != GLFW_RELEASE)
radius = min(1.0, radius + 0.01)
else if (key == GLFW_KEY_S && action != GLFW_RELEASE)
radius = max(0.01, radius - 0.01)
else if (key == GLFW_KEY_A && action != GLFW_RELEASE)
segments = max(3, segments - 1)
else if (key == GLFW_KEY_D && action != GLFW_RELEASE)
// in case causing performance issue, limit segments up to 10e5
segments = min(10e5.toInt(), segments + 1)
}
这里我定义W
和S
控制半径的大小,A
和D
控制分段的数量,需要注意的是必须要对半径和分段数做出检查。半径至少不能为负数,但是如果等于0的话就没有意义了,最大是1,因为OpenGL的坐标系是归一化的;分段数取决于绘画模式,如果使用GL_LINE_LOOP
,则最少两个点才能出一条线;而GL_TRIANGLE_FAN
则至少需要3个点才能产生出一个三角形;原则上分段可以无限大,但是取决于电脑的具体性能,如果分段太大,一方面Float型承载不了那种精度(显卡执行的全都是Float型运算,要执行Double型运算多半得上贵的离谱的专业卡),另一方面计算起来耗时很大,没有必要。
这样一来就可以通过键盘控制画圆的参数了,还挺有意思的。
-全文完-
- 来源:https://gamedev.stackexchange.com/a/133817 这个Stackoverflow回答中提到以
glBegin()
和glEnd()
为代表的渲染技术已经被废弃,虽然还可以使用,但是并不推荐过多使用。由于我没能深入了解他们被废弃的原因,但即便如此LWJGL的官方教程中还是用了这些代码,也许就是用作教程吧。 ↩
【代码札记】使用LWJGL和OpenGL画个圆 由 天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。