天空Blond - LWJGL
type: channel
link: https://www.skyblond.info/tag/LWJGL/
items: 【代码札记】LWJGL's OpenCL HelloWorld
items: 【代码札记】使用 LWJGL 和 OpenGL 最优雅的画圆
items: 【代码札记】使用 LWJGL 和 OpenGL 更优雅的画圆
items: 【代码札记】使用LWJGL和OpenGL画个圆

【代码札记】LWJGL's OpenCL HelloWorld
type: item
date: 2021-04-15T21:47:55+08:00
description: 前几篇文章中简述了OpenGL的入门过程，本文将简述LWJGL提供的OpenCL绑定下，OpenCL的入门过程。OpenCLOpenGL中的GL表示Graphics Library，而OpenCL则是用于通用计算的：Open Computing Language。早期人们使用显卡进行图形渲染，但后来人们逐渐意识到，显卡上每一个运行着色器的单元就如同一个小CPU——它不如正经CPU那么复杂，频率不快，可用内存也少，还缺乏高级的分支预测和乱序执行能力，但它数量多啊。以我新换的雷蛇笔记本为例：CPU是英特尔的i7-10875H，8核16线程，如果在Java中处理CPU密集型的任务，一般会选择17个线程来执行，如果是I/O密集型任务，CPU空闲时间比较多，则选择33个线程来执行。但是GPU呢？对于RTX3070来说，它有5120个CUDA核心，这意味着原则上我们可以同时在GPU中执行5120个代码，即5120个线程。虽然CPU能跑到5.1GHz，而GPU最高1.62GHz，但在需要高并发的任务中，GPU确实脱颖而出。前几天我在Youtube上看到一个视频：「Fully OpenCL accelerated space battle. (rts rpg game)」，视频作者利用OpenCL加速游戏。这个游戏是一个类似太空大战的游戏，是建立在二维平面上的，每艘船的行为使用OpenCL得出，而不再使用CPU遍历处理。这样得到的性能提升是什么呢？可以实时模拟接近7万个实体，游戏没有任何卡顿。但使用OpenCL是有代价的。OpenCL最初设计的目标并非只是为了在GPU上运算，实际上OpenCL的目标是异构计算，即同一段代码可以在CPU、GPU、FPGA中的任意一种上直接运行，而不必做出修改。这意味着OpenCL必须在设计上保证不依赖某一种平台的结构或特性，而提供一种通用的计算结构，因此我们平时习以为常的CPU内存结构和计算结构，在OpenCL中将不复存在。另一方面说，GPU虽然运算快，但是把数据从CPU内存送到GPU内存并不快。普通游戏显卡通常用于渲染游戏画面，即CPU把当前画面的数据送给显卡，显卡进行渲染，随后CPU送来新的一帧数据，数据是只进不出的；而为了实现通用计算，我们必须在运算结束后把数据从GPU内存读回CPU内存，游戏卡并非做不到，但没有经过特殊的设计与优化，会对速度造成一些影响，这一点在文章最后将代码跑起来后能够有所体现。LWJGL's OpenCL由于OpenCL和OpenGL一样是用C写的，因此Java并不能直接调用他们。于是为了拉齐Java与C之间的鸿沟，和OpenGL一样，LWJGL也提供了OpenCL的绑定，使得我们可以像在C语言中那样直接调用对应的函数就可以完成对应的功能。值得一提的是，LWJGL的OpenCL绑定并非最好，也非唯一。实际上隔壁JOCL库做的更加面向对象一些，用起来也更加舒服，而且文档也很丰富。目前LWJGL3看起来正在重构，OpenCL没有任何文档，只能去他们的Github Repo中读一些测试代码，或者参考OpenCL在C语言中的用法，结合OpenGL绑定中与C交互的部分去猜测代码应该如何编写。所以我的建议是，如果不使用LWJGL提供的OpenGL绑定而只需要使用OpenCL，取决于你是否对编写C语言内核（即着色器，实际跑在GPU上的代码）心存芥蒂，你可以使用JOCL来更加面向对象的加载你编写的C内核；或者直接使用Aparapi库将你的Java代码自动翻译成OpenCL需要的形式。本文将使用LWJGL提供的OpenCL绑定，这个绑定更接近原始C接口，用起来更加复杂一些。如无意外，在未来我将使用JOCL进行OpenCL计算，届时会有对应的文章发表。Hello, World闲话少说，书归正传。要使用LWJGL的OpenCL绑定，只需要在其官网配置项目的时候选定OpenCL就可以了。OpenCL内核就一般的开发流程而言，应当是先搭出来程序的架子，然后再去谈什么样的代码跑在GPU上，但是出于演示的目的，这里先给出OpenCL内核：__kernel void demo(__global float *a, __global float *b, __global float *answer) { int xid = get_global_id(0); answer[xid] = cos(a[xid]) + sin(b[xid]); }内核是同时运行在每一个CUDA核心上的程序，因此我们并不能像CPU那样使用循环来完成两个数组的按项求和。正相反，我们要展开这个操作：每一个Cuda核心只负责将数组中的一个索引位置上的数据相加。于是对于每一个核心，我们先要获取OpenGL给这个核心的编号，然后再根据这个编号将对应位…

【代码札记】使用 LWJGL 和 OpenGL 最优雅的画圆
type: item
date: 2021-04-13T21:04:52+08:00
description: 上一篇文章通过使用VBO来改善程序的运行效率，但程序的计算还是在CPU侧执行。本文将使用着色器，将运算放在GPU上，并获得一定的性能提升。前言着色器是运行在GPU上的一段程序。按照OpenGL对渲染流水线的定义，本文我们将自定义两个着色器：顶点着色器用于计算圆上的每一个点的坐标，片段着色器用于计算所有待呈现片段的颜色。通过操作这两个着色器，我们能够实现利用显卡计算每一个点的坐标，并给最终画出来的圆上色。在这个过程中CPU不再进行一大堆三角函数的运算，而更注重程序运行流程的控制。预备着色器在使用着色器之前，先创建一个工具类：package info.skyblond.antSimu.opengl import org.lwjgl.opengl.GL20.* import org.lwjgl.system.MemoryUtil import java.nio.FloatBuffer import java.nio.charset.StandardCharsets class Shader( private val filename: String ) { private val program: Int private val vertexesShader: Int private val fragmentShader: Int init { program = glCreateProgram() vertexesShader = glCreateShader(GL_VERTEX_SHADER) glShaderSource(vertexesShader, readShaderFileContent("/shaders/$filename.vert")) glCompileShader(vertexesShader) require(glGetShaderi(vertexesShader, GL_COMPILE_STATUS) == 1) { "Unable to compile vertex shader: '/shaders/$filename.vert':" + "\n${glGetShaderInfoLog(vertexesShader)}" } fragmentShader = glCreateShader(GL_FRAGMENT_SHADER) glShaderSource(fragmentShader, readShaderFileContent("/shaders/$filename.frag")) glCompileShader(fragmentShader) require(glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == 1) { "Unable to compile fragment shader: '/shaders/$filename.frag'" + "\n${glGetShaderInfoLog(fragmentShader)}" } glAttachShader(program, vertexesShader) glAttachShader(program, fragmentShader) glBindAttribLocation(program, 0, "index") glLinkProgram(program) require(glGetProgrami(program, GL_LINK_STATUS) == 1) { "Failed to link program:" + "\n${glGetProgramInfoLog(program)}" } glValidateProgram(program) require(glGetProgrami(program, GL_VALIDATE_STATUS) == 1) { "Failed to validate program:" + "\n${glGetProgramInfoLog(program)}" } } fun setUniform(name: String, value: Int) { val location = glGetUniformLocation(program, name) require(location != -1) {"Invalidate location returned"} glUniform1i(location, value) } fun setUniform(name: String, value: Double) { val location = glGetUniformLocation(program, name) require(location …

【代码札记】使用 LWJGL 和 OpenGL 更优雅的画圆
type: item
date: 2021-04-07T21:27:00+08:00
description: 上一篇文章记录了我初学OpenGL，使用LWJGL在JVM上试图画圆的过程。虽然结果很成功，甚至还锦上添花地弄了一个五彩斑斓的圆，但随着更加深入的学习，我了解到上一篇文章中的代码应该算得上是OpenGL中最低效的绘图方法。因此本文将试图使用VBO改进代码的效率。前言为什么上一篇代码就那么低效呢？原因就在于Fixed Pipeline中提供的绘制函数与GPU内存的交互方式上。glVertex2f()函数能够将我们定义的顶点坐标写入到显卡内存，这样后续的流水线作业就可以在显卡上并行执行以加速运算速度。但是这个函数有一个问题：我们每调用一次这个函数，它就将坐标数据写给显卡，这样做是很低效的。一般而言最佳的做法是集中一批数据，然后统一送给显卡，这样更能充分发挥PCIe通道的并行性。OpenGL在一开始提供了Fixed Pipeline，大概是因为便于上手，虽然还是比较繁琐，但至少还能提供一些易于理解的地方。倘若上来就从窗口跳到VBO、VAO和着色器（例如目前OpenGL的教程），怕不是要劝退好多人。不过由于这种固定的流水线效率低局限多，在OpenGL core 3.0的更高版本已经被废弃了。如果你需要使用OpenGL3.0的特性，那么上一篇文章的代码实际是不能运行的。但目前使用的LWJGL库使用的OpenGL版本是3.2.3，为什么还能不报错运行呢？也许有细心的读者已经注意到了，我们在上一篇文章中的import中有GL11和GL15的字样，意味着我们实际使用的是OpenGL1.1和1.5时期的特性。目前为止OpenGL分为两个部分，在OpenGL 3.0时期引入，将原本的OpenGL一分为二：一部分是OpenGL core，如字面意思，它只包含本版本应该支持的特性（目前来看就是程序只能通过Shader编写，而不能使用固定流水线），如果试图在core中调用已经移除的接口，OpenGL将会产生错误；而另一部分则是Compatibility Profile，它负责实现所有过去的接口（我们调用的固定流水线的函数实际是在这里实现的），以便保证使用以前版本构建的代码能够在更新版本上顺利运行。就目前而言，我们距离着色器还差一些。如开头所言，本文的目的是要将代码改写成将数据按批次送给显卡，而不是你给一个坐标我就立刻送一个。使用VBOVBO全称Vertex Buffer Objects，翻译成中文是「顶点缓冲对象」，该对象负责管理一部分GPU内存，在这部分GPU内存中存储大量的顶点数据，便于让CPU一次能够发送大量数据，使得程序更有效率。创建VBO如何使用这个VBO呢？首先得有一个： // prepare our vertex buffer, VBO val vertexVBO = glGenBuffers()然而这样只是让OpenGL在显存中开辟了内存空间，我们的Java程序是没办法直接使用的，因此还需要再Java程序中开辟一个内存空间。LWJGL使用Java NIO的Buffer来进行数据交换：程序将数据写入Java NIO Buffer，然后调用glBufferData将给定Buffer中的数据复制到VBO管理的显存中。创建NIO Buffer于是我们还需要一个NIO Buffer： // float buffer used to write into the vbo var floatBuffer: FloatBuffer关于这个NIO Buffer还有一个使用的问题：向Buffer中写入数据主要有两种方法：put(float[] src) put(int index, float f)这两种方法虽然都能够把数据写入到Buffer中，但产生的副作用不同。控制Buffer读写的内部变量主要有两个：limit和position。直接将数组写入Buffer，将会同时更新这两个参数，于是写入数据后position总是指向最后的写入数据，如果这个时候把Buffer丢给glBufferData给OpenGL，那么它读不到前面写入的数据，为了解决这个问题，Buffer有一个flip()函数，这个函数的作用就是将limit设定为当前的position的值，然后把position的值清零，这样再交给OpenGL读就可以正常读到刚刚写入的数据了。所以一般经常把第一种方法和flip()函数配合使用。如果使用第二种方式写入数据，则二者都不会修改，于是需要调用者手动根据写入的值的个数和开始写时Buffer的状态，手动更新其参数，一般而言比较麻烦，但好处是不需要另外将数据写入数组，再将数组写入Buffer。本文为了省事儿，将使用第二种方法，如果读者使用第一种方法，请务必不要忘记在put之后调用flip。由于segements会动态改变，所以Buffer的大小不能定死，需要…

【代码札记】使用LWJGL和OpenGL画个圆
type: item
date: 2021-04-04T22:29:00+08:00
description: 前些日子在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 callb…