はじめに
OpenGLは様々な言語でバインディングが提供されています。また簡単なプログラムでもFizzBuzzやフィボナッチ数列などのアルゴリズム問題よりも多少複雑なものになるので、OpenGLを通じて各プログラミング言語のスタイルに慣れることができます。ということで、今回はC++、Python、Haskell、Rust、Go、Scheme、Common Lisp、番外編としてJavaScriptでTriangle Gradient(図1)を描画するプログラムをそれぞれの言語のスタイルに合わせて書き、それを紹介していきたいと思います。コンセプトとしてGLUTは使わずGLFW+OpenGLによるモダンなOpenGLプログラミングを目標としています。この記事をみてOpenGLに興味を持った方はぜひご自身の得意とする言語で別の記事を投稿してみてください。
今回出したプログラムはすべてここに乗っているので興味があればぜひ見ていってください。
C++
# include <fstream>
# include <iostream>
# include <vector>
# include <GL/glew.h>
# include <GLFW/glfw3.h>
# include <glm/glm.hpp>
class shader_program {
public:
shader_program() {
m_handle = glCreateProgram();
}
shader_program(const shader_program& obj) = delete;
shader_program(shader_program&& obj) noexcept {
if(this != &obj) {
reset();
m_handle = obj.m_handle;
obj.m_handle = 0;
}
}
~shader_program() {
reset();
}
[[nodiscard]] bool enabled() const noexcept {
return m_handle != 0;
}
[[nodiscard]] GLuint handle() const noexcept {
return m_handle;
}
void reset() {
if(enabled()) {
glDeleteProgram(handle());
m_handle = 0;
}
}
bool add_shader(const std::string &src, GLenum type) {
if(!enabled()) return false;
GLuint shader = glCreateShader(type);
auto source = src.data();
GLint length = src.size();
glShaderSource(shader, 1, &source, &length);
glCompileShader(shader);
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if(status == GL_FALSE) {
GLsizei size; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &size);
std::string log(size, 0);
glGetShaderInfoLog(shader, log.length(), &size, log.data());
std::cerr << log << std::endl;
return false;
}
glAttachShader(handle(), shader);
glDeleteShader(shader);
return true;
}
bool link() {
if(!enabled()) return false;
glLinkProgram(handle());
GLint status;
glGetProgramiv(handle(), GL_LINK_STATUS, &status);
if(status == GL_FALSE) {
GLsizei size; glGetProgramiv(handle(), GL_INFO_LOG_LENGTH, &size);
std::string log(size, 0);
glGetProgramInfoLog(handle(), log.length(), &size, log.data());
std::cerr << log << std::endl;
return false;
}
return true;
}
void use() const {
glUseProgram(handle());
}
void unuse() const {
glUseProgram(0);
}
private:
GLuint m_handle;
};
struct vertex_t {
glm::vec2 position;
glm::vec4 color;
};
int main() {
if(glfwInit() == GLFW_FALSE) {
throw std::runtime_error("failed to initialize GLFW");
}
std::atexit(glfwTerminate);
auto window = glfwCreateWindow(700, 700, "triangle", nullptr, nullptr);
if(window == nullptr) {
throw std::runtime_error("failed to create GLFWwindow");
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwMakeContextCurrent(window);
if(glewInit() != GLEW_OK) {
throw std::runtime_error("failed to initialize GLEW");
}
std::vector<vertex_t> vertex {
{ { 0, 1 }, { 1, 0, 0, 1 } },
{ { -1, -1 }, { 0, 1, 0, 1 } },
{ { 1, -1 }, { 0, 0, 1, 1 } }
};
GLuint vtx;
glGenBuffers(1, &vtx);
glBindBuffer(GL_ARRAY_BUFFER, vtx);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_t) * vertex.size(), vertex.data(), GL_STATIC_DRAW);
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vtx);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (GLvoid*)(offsetof(vertex_t, position)));
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (GLvoid*)(offsetof(vertex_t, color)));
glBindVertexArray(0);
shader_program program;
if(std::fstream fin("../shaders/triangle.vert", std::ios::in); fin.good()) {
program.add_shader(std::string { std::istreambuf_iterator<char>{fin}, std::istreambuf_iterator<char>{} }, GL_VERTEX_SHADER);
}
if(std::fstream fin("../shaders/triangle.frag", std::ios::in); fin.good()) {
program.add_shader(std::string { std::istreambuf_iterator<char>{fin}, std::istreambuf_iterator<char>{} }, GL_FRAGMENT_SHADER);
}
program.link();
glClearColor(0, 0, 0, 1);
while(glfwWindowShouldClose(window) == GLFW_FALSE) {
glClear(GL_COLOR_BUFFER_BIT);
program.use();
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
program.unuse();
glfwSwapBuffers(window);
glfwWaitEvents();
}
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &vtx);
return 0;
}
備考
OpenGLプログラミングでは一番オーソドックスな言語だと思います。リファレンスや先人たちの足跡もしっかりと残っておりOpenGLを学ぶならC++一択と言っても過言ではないでしょう。余談ですがシェーダの作り方について、多くの記事ではシェーダのコンパイルとリンクをバーテクスシェーダ・フラグメントシェーダで同じ関数の中で行っているようですが、様々なシェーダを扱う際にあまり汎用性がないんじゃないかなーと思ってシェーダ1種類ごとにコンパイルとアタッチを行う形式にしています。ちなみに同じ思想のもとで書いたC++用OpenGLラッパーライブラリを書いているのでご興味があればぜひ。
Python
from ctypes import Structure, sizeof
import sys
import atexit
from OpenGL.GL import *
import glfw
class Shader:
def __init__(self):
self.handle = glCreateProgram()
def attach_shader(self, content, type, log_always=False):
shader = glCreateShader(type)
glShaderSource(shader, [content])
glCompileShader(shader)
status = ctypes.c_uint(GL_UNSIGNED_INT)
glGetShaderiv(shader, GL_COMPILE_STATUS, status)
if log_always or not status:
print(glGetShaderInfoLog(shader).decode("utf-8"), file=sys.stderr)
glDeleteShader(shader)
return False
glAttachShader(self.handle, shader)
glDeleteShader(shader)
return True
def link(self, log_always=False):
glLinkProgram(self.handle)
status = ctypes.c_uint(GL_UNSIGNED_INT)
glGetProgramiv(self.handle, GL_LINK_STATUS, status)
if log_always or not status:
print(glGetProgramInfoLog(self.handle).decode("utf-8"), file=sys.stderr)
return False
return True
def use(self):
glUseProgram(self.handle)
def unuse(self):
glUseProgram(0)
class Vertex(Structure):
_fields_ = [
('position', GLfloat * 2),
('color', GLfloat * 4)
]
def main():
if not glfw.init():
raise RuntimeError("failed to initialize GLFW")
atexit.register(glfw.terminate)
window = glfw.create_window(700, 700, "triangle", None, None)
if not window:
raise RuntimeError("failed to create GLFWwindow")
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4)
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 6)
glfw.make_context_current(window)
vertex = (Vertex * 3)(
Vertex((0, 1), (1, 0, 0, 1)),
Vertex((-1, -1), (0, 1, 0, 1)),
Vertex((1, -1), (0, 0, 1, 1))
)
vtx = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, vtx)
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW)
vao = glGenVertexArrays(1)
glBindVertexArray(vao)
glBindBuffer(GL_ARRAY_BUFFER, vtx)
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), GLvoidp(Vertex.position.offset))
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), GLvoidp(Vertex.color.offset))
glBindVertexArray(0)
program = Shader()
with open("../shaders/triangle.vert", "r") as f:
program.attach_shader(f.read(), GL_VERTEX_SHADER)
with open("../shaders/triangle.frag", "r") as f:
program.attach_shader(f.read(), GL_FRAGMENT_SHADER)
program.link()
glClearColor(0, 0, 0, 1)
while glfw.window_should_close(window) == glfw.FALSE:
glClear(GL_COLOR_BUFFER_BIT)
program.use()
glBindVertexArray(vao)
glDrawArrays(GL_TRIANGLES, 0, 3)
glBindVertexArray(0)
program.unuse()
glfw.swap_buffers(window)
glfw.wait_events()
glDeleteProgram(program.handle)
glDeleteVertexArrays(1, [vao])
glDeleteBuffers(1, [vtx])
if __name__ == "__main__":
main()
備考
PythonもC++とあまり変わりません。ドキュメントもしっかりしていますし、C言語ライクな型が簡単に定義できるのでOpenGL用のデータを定義するのも明快です。個人的に今からOpenGLを学ぶならPythonが一番ラクなんじゃないかと思っています。
Haskell
module Main where
import Control.Monad (unless, void, when)
import Control.Monad.Reader (ReaderT, runReaderT, ask, asks)
import Control.Monad.State (liftIO)
import Foreign.Marshal.Array (withArray)
import Foreign.Ptr (plusPtr, nullPtr, Ptr)
import qualified Data.ByteString as B
import qualified Graphics.Rendering.OpenGL as GL
import Graphics.Rendering.OpenGL (($=))
import qualified Graphics.UI.GLFW as GLFW
import Lib
import Shader
data GLHandle = GLHandle
{ window :: !GLFW.Window
, vtx :: !GL.BufferObject
, vao :: !GL.VertexArrayObject
, program :: !GL.Program
}
type Env = ReaderT GLHandle IO
genVbo :: IO GL.BufferObject
genVbo = GL.genObjectName >>= return
genVao :: IO GL.VertexArrayObject
genVao = GL.genObjectName >>= return
offset :: Integral a => a -> Ptr b
offset = plusPtr nullPtr . fromIntegral
loopEnv :: Env a -> GLHandle -> IO a
loopEnv f handle = do
runReaderT f handle
loop :: Env ()
loop = do
window <- asks window
vao <- asks vao
program <- asks program
liftIO $ do
GL.clear [GL.ColorBuffer]
GL.bindVertexArrayObject $= Just vao
GL.currentProgram $= Just program
GL.drawArrays GL.Triangles 0 3
GL.currentProgram $= Nothing
GL.bindVertexArrayObject $= Nothing
GLFW.swapBuffers window
GLFW.waitEvents
r <- liftIO $ GLFW.windowShouldClose window
unless r loop
main :: IO ()
main = do
r <- GLFW.init
when r $ do
m <- GLFW.createWindow 700 700 "triangle" Nothing Nothing
case m of
(Just window) -> do
GLFW.windowHint $ GLFW.WindowHint'ContextVersionMajor 4
GLFW.windowHint $ GLFW.WindowHint'ContextVersionMinor 6
GLFW.makeContextCurrent m 。
-- | position | | color |
let vertex = [ 0, 1, 1, 0, 0, 1
, -1, -1, 0, 1, 0, 1
, 1, -1, 0, 0, 1, 1] :: [GL.GLfloat]
vbo <- genVbo
GL.bindBuffer GL.ArrayBuffer $= Just vbo
withArray vertex $ \ptr -> do
let bufferSize = fromIntegral $ 4 * (length vertex)
GL.bufferData GL.ArrayBuffer $= (bufferSize, ptr, GL.StaticDraw)
vao <- genVao
GL.bindVertexArrayObject $= Just vao
let verLoc = GL.AttribLocation 0
colLoc = GL.AttribLocation 1
GL.vertexAttribArray verLoc $= GL.Enabled
GL.vertexAttribArray colLoc $= GL.Enabled
GL.vertexAttribPointer verLoc $= (GL.ToFloat, GL.VertexArrayDescriptor 2 GL.Float (4 * 6) (offset 0))
GL.vertexAttribPointer colLoc $= (GL.ToFloat, GL.VertexArrayDescriptor 4 GL.Float (4 * 6) (offset 8))
GL.bindVertexArrayObject $= Nothing
program <- GL.createProgram
B.readFile "../shaders/triangle.vert" >>= attach program GL.VertexShader
B.readFile "../shaders/triangle.frag" >>= attach program GL.FragmentShader
GL.linkProgram program
let handle = GLHandle { window = window
, vtx = vbo
, vao = vao
, program = program }
loopEnv loop handle
GL.deleteObjectName program
GL.deleteObjectName vao
GL.deleteObjectName vbo
Nothing -> return ()
GLFW.terminate
module Shader (
attach
) where
import Data.ByteString
import qualified Graphics.Rendering.OpenGL as GL
import Graphics.Rendering.OpenGL (($=))
attach :: GL.Program -> GL.ShaderType -> ByteString -> IO ()
attach program tp src = do
shader <- GL.createShader tp
GL.shaderSourceBS shader $= src
GL.compileShader shader
GL.attachShader program shader
GL.deleteObjectName shader
備考
個人的に今回一番推したい言語です。まだモナドに慣れていないので間違いがあるかもしれませんが、アプリケーション全体で値を共有したいときにReaderTモナドを使えば良いというのは非常に勉強になりました。これのおかげで初期化部分とメインループを分離することができ、コードの見通しも良くなります。またOpenGL自体についても$=で値を渡していくのが新鮮で、わかりやすくきれいなコードが書けるライブラリだなあと終始感嘆していました。多分今回紹介するライブラリの中では一番完成度が高いバインディングな気がします。超オススメです!
Rust
extern crate gl;
extern crate glfw;
use gl::types::*;
use glfw::Context;
use std::{mem, ptr};
use std::fs::File;
use std::io::prelude::*;
use std::ffi::CString;
struct Shader {
m_handle: GLuint,
}
impl Shader {
pub fn new() -> Self {
unsafe {
Self { m_handle: gl::CreateProgram() }
}
}
pub fn handle(&self) -> GLuint {
self.m_handle
}
pub fn attach_shader(&self, src: &String, tp: GLenum) {
unsafe {
let shader = gl::CreateShader(tp);
let cs = CString::new(src.as_bytes()).unwrap();
gl::ShaderSource(shader, 1, &mut cs.as_ptr(), ptr::null());
gl::CompileShader(shader);
let mut status = gl::FALSE as GLint;
gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut status);
if status != (gl::TRUE as GLint) {
let mut len = 0;
gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut len);
let mut buf = vec![0u8; len as usize - 1];
gl::GetShaderInfoLog(shader, len, ptr::null_mut(), buf.as_mut_ptr() as *mut GLchar);
panic!("{}", String::from_utf8(buf).expect("ShaderInfoLog not valid utf8"));
}
gl::AttachShader(self.handle(), shader);
gl::DeleteShader(shader)
}
}
pub fn link(&self) {
unsafe {
gl::LinkProgram(self.handle());
let mut status = gl::FALSE as GLint;
gl::GetProgramiv(self.handle(), gl::LINK_STATUS, &mut status);
if status != (gl::TRUE as GLint) {
let mut len = 0;
gl::GetProgramiv(self.handle(), gl::INFO_LOG_LENGTH, &mut len);
let mut buf = vec![0u8; len as usize - 1];
gl::GetProgramInfoLog(self.handle(), len, ptr::null_mut(), buf.as_mut_ptr() as *mut GLchar);
panic!("{}", String::from_utf8(buf).expect("ShaderInfoLog not valid utf8"));
}
}
}
pub fn use_(&self) {
unsafe {
gl::UseProgram(self.handle());
}
}
pub fn unuse(&self) {
unsafe {
gl::UseProgram(0);
}
}
}
impl Drop for Shader {
fn drop(&mut self) {
if self.m_handle != 0 {
unsafe {
gl::DeleteProgram(self.m_handle);
self.m_handle = 0;
}
}
}
}
fn main() {
let mut glfw = glfw::init(glfw::FAIL_ON_ERRORS).unwrap();
let (mut window, _) = glfw.create_window(700, 700, "triangle", glfw::WindowMode::Windowed)
.expect("Failed to create GLFWwindow");
glfw.window_hint(glfw::WindowHint::ContextVersion(4, 6));
window.make_current();
gl::load_with(|symbol| window.get_proc_address(symbol) as *const _);
let vertex: [GLfloat; 18] = [
0.0, 1.0, 1.0, 0.0, 0.0, 1.0,
-1.0, -1.0, 0.0, 1.0, 0.0, 1.0,
1.0, -1.0, 0.0, 0.0, 1.0, 1.0
];
let mut vtx: GLuint = 0;
let mut vao: GLuint = 0;
unsafe {
gl::GenBuffers(1, &mut vtx);
gl::BindBuffer(gl::ARRAY_BUFFER, vtx);
gl::BufferData(gl::ARRAY_BUFFER, (vertex.len() * mem::size_of::<GLfloat>()) as GLsizeiptr, mem::transmute(&vertex[0]), gl::STATIC_DRAW);
gl::GenVertexArrays(1, &mut vao);
gl::BindVertexArray(vao);
gl::BindBuffer(gl::ARRAY_BUFFER, vtx);
gl::EnableVertexAttribArray(0);
gl::EnableVertexAttribArray(1);
gl::VertexAttribPointer(0, 2, gl::FLOAT, gl::FALSE as GLboolean, 24, ptr::null());
gl::VertexAttribPointer(1, 4, gl::FLOAT, gl::FALSE as GLboolean, 24, 8 as *const GLvoid);
}
let program = Shader::new();
let mut vs = String::new();
let mut fp1 = File::open("../shaders/triangle.vert").expect("file not found");
fp1.read_to_string(&mut vs)
.expect("failed to read vertex shader");
program.attach_shader(&vs, gl::VERTEX_SHADER);
let mut fs = String::new();
let mut fp2 = File::open("../shaders/triangle.frag").expect("file not found");
fp2.read_to_string(&mut fs)
.expect("failed to read fragment shader");
program.attach_shader(&fs, gl::FRAGMENT_SHADER);
program.link();
unsafe {
gl::ClearColor(0.0, 0.0, 0.0, 1.0);
}
while !window.should_close() {
unsafe {
gl::Clear(gl::COLOR_BUFFER_BIT);
program.use_();
gl::BindVertexArray(vao);
gl::DrawArrays(gl::TRIANGLES, 0, 3);
gl::BindVertexArray(0);
program.unuse();
}
window.swap_buffers();
glfw.wait_events();
}
unsafe {
gl::DeleteVertexArrays(1, &mut vao);
gl::DeleteBuffers(1, &mut vtx);
}
}
備考
なんだかunsafeな部分が多くなってしまいましたが、これでいいのだろうか...。設計はC++のものを流用しましたが大体同じようにかけます。Rustではデフォルトの代入操作がmoveなようなのでShaderの定義部分は結構ラクできました。ただファイル入力の部分がもっと簡単にかけないものかと悔いが残る結果となりました。
Go
package main
import (
"fmt"
"strings"
"io/ioutil"
"runtime"
"github.com/go-gl/gl/v4.6-core/gl"
"github.com/go-gl/glfw/v3.3/glfw"
)
func attachShader(program uint32, src string, tp uint32) error {
shader := gl.CreateShader(tp)
csources, free := gl.Strs(src)
gl.ShaderSource(shader, 1, csources, nil)
free()
gl.CompileShader(shader)
var status int32
gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
if status == gl.FALSE {
var logLength int32
gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)
log := strings.Repeat("\x00", int(logLength + 1))
gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))
return fmt.Errorf("%v", log)
}
gl.AttachShader(program, shader)
gl.DeleteShader(shader)
return nil
}
func init() {
runtime.LockOSThread()
}
func main() {
if err := glfw.Init(); err != nil {
panic(err)
}
defer glfw.Terminate()
window, err := glfw.CreateWindow(700, 700, "triangle", nil, nil)
if err != nil {
panic(err)
}
glfw.WindowHint(glfw.ContextVersionMajor, 4)
glfw.WindowHint(glfw.ContextVersionMinor, 6)
window.MakeContextCurrent()
if err := gl.Init(); err != nil {
panic(err)
}
vertex := []float32 {
0, 1, 1, 0, 0, 1,
-1, -1, 0, 1, 0, 1,
1, -1, 0, 0, 1, 1,
}
var vbo uint32
gl.GenBuffers(1, &vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(vertex) * 4, gl.Ptr(vertex), gl.STATIC_DRAW)
defer gl.DeleteBuffers(1, &vbo)
var vao uint32
gl.GenVertexArrays(1, &vao)
gl.BindVertexArray(vao)
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
gl.EnableVertexAttribArray(0)
gl.EnableVertexAttribArray(1)
gl.VertexAttribPointerWithOffset(0, 2, gl.FLOAT, false, 24, 0)
gl.VertexAttribPointerWithOffset(1, 4, gl.FLOAT, false, 24, 8)
gl.BindVertexArray(0)
defer gl.DeleteVertexArrays(1, &vao)
program := gl.CreateProgram()
bytes, _ := ioutil.ReadFile("../shaders/triangle.vert")
attachShader(program, string(bytes) + "\x00", gl.VERTEX_SHADER)
bytes, _ = ioutil.ReadFile("../shaders/triangle.frag")
attachShader(program, string(bytes) + "\x00", gl.FRAGMENT_SHADER)
gl.LinkProgram(program)
defer gl.DeleteProgram(program)
gl.ClearColor(0, 0, 0, 1)
for !window.ShouldClose() {
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.BindVertexArray(vao)
gl.UseProgram(program)
gl.DrawArrays(gl.TRIANGLES, 0, 3)
gl.UseProgram(0)
gl.BindVertexArray(0)
window.SwapBuffers()
glfw.WaitEvents()
}
}
備考
チョー簡単です。つまりどころは全くありません。C++やPythonと同じように書けばプログラムができます。ここはGoらしいわかりやすさを重視したところが出ているのかもしれません。またdeferを用いることで終了処理も初期化の部分とくっつけて書くことができC++のRAIIとはまた違った優しさがあります。
Scheme
(use gauche.uvector)
(use gl)
(use gl.glfw)
(define (read-file file-name)
(let ((p (open-input-file file-name)))
(let loop((ls1 '()) (c (read-char p)))
(if (eof-object? c)
(begin
(close-input-port p)
(list->string (reverse ls1)))
(loop (cons c ls1) (read-char p))))))
(define (attach-shader program src type)
(let1 shader (gl-create-shader type)
(gl-shader-source shader (list src))
(gl-compile-shader shader)
(gl-attach-shader program shader)
(gl-delete-shader shader)))
(define (main args)
(unless (glfw-init)
(display "failed to initialize GLFW" (current-error-port))
(exit 1))
(let ((window (glfw-create-window 700 700 "triangle" #f #f)))
(unless window
(display "failed to create GLFWwindow" (current-error-port))
(exit 1))
(glfw-window-hint GLFW_CONTEXT_VERSION_MAJOR 4)
(glfw-window-hint GLFW_CONTEXT_VERSION_MAJOR 6)
(glfw-make-context-current window)
(let ((vtx (gl-gen-buffers 1))
(vao (gl-gen-vertex-arrays 1))
(program (gl-create-program))
(vertex '#f32( 0 1 1 0 0 1
-1 -1 0 1 0 1
1 -1 0 0 1 1)))
(gl-bind-vertex-array (uvector-ref vao 0))
(gl-bind-buffer GL_ARRAY_BUFFER (uvector-ref vtx 0))
(gl-buffer-data GL_ARRAY_BUFFER (* 18 4) vertex GL_STATIC_DRAW)
(gl-enable-vertex-attrib-array 0)
(gl-enable-vertex-attrib-array 1)
(gl-vertex-attrib-pointer 0 2 GL_FLOAT #f 24 0)
(gl-vertex-attrib-pointer 1 4 GL_FLOAT #f 24 8)
(gl-bind-vertex-array 0)
(attach-shader program (read-file "../shaders/triangle.vert") GL_VERTEX_SHADER)
(attach-shader program (read-file "../shaders/triangle.frag") GL_FRAGMENT_SHADER)
(gl-link-program program)
(gl-point-size 10)
(gl-clear-color 0 0 0 1)
(let loop ()
(unless (glfw-window-should-close window)
(gl-clear GL_COLOR_BUFFER_BIT)
(gl-use-program program)
(gl-bind-vertex-array (uvector-ref vao 0))
(gl-draw-arrays GL_TRIANGLES 0 3)
(gl-bind-vertex-array 0)
(gl-use-program 0)
(glfw-swap-buffers window)
(glfw-wait-events)
(loop)))
(gl-delete-program program)
(gl-delete-buffers vtx)
(gl-delete-vertex-arrays vao)
(glfw-destroy-window window))
(glfw-terminate)
(exit 0)))
備考
正直今回扱った言語の中で一番大変でした。何が大変なのかというとまず環境構築です。Ubuntuで書いていたのですがaptで配布されているGaucheが古いため最新のGauche-glがビルドできません。そのためgithubからクローンしてきてビルドしなければならないわけです。さらに最新のGauche-glにはOpenGL3以降の新しいAPIがないため自分で必要な関数を書かなければなりませんでした。ここらへんの苦労は別の記事にまとめたいと思います。
以上のことがあったので残念ながら上記のコードはGauche-glのgithubのHEADでも動きません。ただ書き心地の方は非常に良かったです。C言語のように手続き的に書くこともできますし、様々な便利関数があるのでよりフローがわかりやすいコードが書けるな、という印象です。
Common Lisp
(require 'cffi)
(require 'cl-opengl)
(require 'cl-glfw3)
(defvar *vtx*)
(defvar *vao*)
(defvar *program*)
(defun read-file (infile)
(with-open-file (instream infile :direction :input :if-does-not-exist nil)
(when instream
(let ((string (make-string (file-length instream))))
(read-sequence string instream)
string))))
(defun attach-shader (program src type)
(let ((shader (gl:create-shader type)))
(gl:shader-source shader src)
(gl:compile-shader shader)
(gl:attach-shader program shader)))
(defun render ()
(gl:clear :color-buffer)
(gl:use-program *program*)
(gl:bind-vertex-array *vao*)
(gl:draw-arrays :triangles 0 3)
(gl:bind-vertex-array 0)
(gl:use-program 0))
(defun main ()
(glfw:with-init-window (:width 700 :height 700 :title "triangle"
:context-version-major 4
:context-version-minor 6)
(let ((buffers (gl:gen-buffers 1))
(vertex (gl:alloc-gl-array :float 18))
(data #( 0.0 1.0 1.0 0.0 0.0 1.0
-1.0 -1.0 0.0 1.0 0.0 1.0
1.0 -1.0 0.0 0.0 1.0 1.0)))
(setf *vtx* (elt buffers 0))
(dotimes (i (length data))
(setf (gl:glaref vertex i) (aref data i)))
(gl:bind-buffer :array-buffer *vtx*)
(gl:buffer-data :array-buffer :static-draw vertex)
(gl:free-gl-array vertex))
(setf *vao* (gl:gen-vertex-array))
(gl:bind-vertex-array *vao*)
(gl:bind-buffer :array-buffer *vtx*)
(gl:enable-vertex-attrib-array 0)
(gl:enable-vertex-attrib-array 1)
(gl:vertex-attrib-pointer 0 2 :float nil 24 (cffi:null-pointer))
(gl:vertex-attrib-pointer 1 4 :float nil 24 (cffi:make-pointer 8))
(gl:bind-vertex-array 0)
(setf *program* (gl:create-program))
(attach-shader *program* (read-file "../shaders/triangle.vert") :vertex-shader)
(attach-shader *program* (read-file "../shaders/triangle.frag") :fragment-shader)
(gl:link-program *program*)
(gl:clear-color 0 0 0 1)
(loop until (glfw:window-should-close-p)
do (render)
do (glfw:swap-buffers)
do (glfw:wait-events))
(gl:delete-program *program*)
(gl:delete-buffers (list *vtx*))
(gl:delete-vertex-arrays (list *vao*))))
備考
Schemeとは打って変わってなんの詰まりもなく書き上げることができました。やっぱりドキュメントって大事。ただ個人的な趣味嗜好の問題ですが、Schemeのほうが書きやすいと感じました。おそらく書くのに必要な知識や情報量がSchemeのほうが少ないからだと思います。それでもやはりそこはCommon Lisp、glfw:with-init-windowのようなglfwの初期化の仕方は魅力的に感じました。もうちょっとアプリを書けば多分ハマります。
番外編 JavaScript
今回のコンセプトがGLFW+OpenGLだったのでWebGLを使うJavaScriptは番外編となっています。
<html>
<head>
<title>triangle</title>
<script type="text/javascript" src="main.js"></script>
</head>
<body>
<canvas id="scene" width="700" height=700></canvas>
</body>
</html>
vs = `#version 300 es
in vec2 vertex;
in vec4 color;
out vec4 vertexColor;
void main() {
gl_Position = vec4(vertex, 0, 1);
vertexColor = color;
}
`;
fs = `#version 300 es
precision highp float;
in vec4 vertexColor;
out vec4 outColor;
void main() {
outColor = vertexColor;
}
`;
function attachShader(gl, program, src, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
}
gl.attachShader(program, shader);
gl.deleteShader(shader);
}
function main() {
const canvas = document.querySelector("#scene");
const gl = canvas.getContext("webgl2");
if(gl === null) {
alert("failed initialize WebGL");
return ;
}
const vertex = [
0.0, 1.0, 1.0, 0.0, 0.0, 1.0,
-1.0, -1.0, 0.0, 1.0, 0.0, 1.0,
1.0, -1.0, 0.0, 0.0, 1.0, 1.0
];
const program = gl.createProgram();
attachShader(gl, program, vs, gl.VERTEX_SHADER);
attachShader(gl, program, fs, gl.FRAGMENT_SHADER);
gl.linkProgram(program);
const vtx = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vtx);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertex), gl.STATIC_DRAW);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const loc1 = gl.getAttribLocation(program, "vertex");
const loc2 = gl.getAttribLocation(program, "color");
gl.enableVertexAttribArray(loc1);
gl.enableVertexAttribArray(loc2);
gl.vertexAttribPointer(loc1, 2, gl.FLOAT, false, 24, 0);
gl.vertexAttribPointer(loc2, 4, gl.FLOAT, false, 24, 8);
gl.bindVertexArray(null);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.bindVertexArray(null);
gl.useProgram(null);
}
window.onload = main;
備考
いつものWebGLですね。何も言うところはありません。というかWebGLでTriangle Gradientって記事は世の中に溢れているのでわざわざやる必要もなかったような気がしますが、とりあえずということで。
まとめ
今回は様々な言語でOpenGLによるTriangle Gradientの描画を行いました。各言語の特色が出ていて非常に学びになりましたし、特にHaskellやSchemeは今後もOpenGLを使ったアプリを書いてみたいと思えるくらい魅力的な言語でした。今後は別の言語についても見てみたいですね。ForthとかNimとかOcamlとか。