4k intro 作ります
4k introは、C言語を書いて作ります。なので、C言語を使えない人間には、敷居の高いものと思いこんできました。実際作ってみるとC言語のスキルは、ほとんど要りません。文法もGLSLと同じようなので読むにも困りません。テンプレートさえ手に入れればwebGLでshaderが書けるなら作れるモノでした。
とりあえず作ってみます。その前にソースをざーと眺めてください。
// demo.c
#include <windows.h>
#include <GL/gl.h>
#include <GL/glext.h>
#include <mmreg.h>
static const char *vsh = \
"void main()"
"{"
"gl_Position = gl_Vertex;"
"}";
static const char *fsh = \
"uniform vec2 resolution;"
"uniform float time;"
"void main()"
"{"
"vec2 p = (2.0 * gl_FragCoord.xy - resolution.xy) / resolution.y;"
"p += vec2(cos(time), sin(time)) * 0.5;"
"float g = exp(-1.5 * dot(p,p));"
"gl_FragColor = vec4(g, g, g, 1.0);"
"}";
const char *msh = \
"#version 330\n"
"out vec2 gain;"
"uniform float sampleRate;"
"void main()"
"{"
"float time = float(gl_VertexID) / sampleRate;"
"gain = vec2(sin(6.2831*440.0*fract(time)));"
"}";
#define U_RESOLUTION "resolution"
#define U_SAMPLERATE "sampleRate"
#define U_TIME "time"
#define O_GAIN "gain"
#define XRES 1920
#define YRES 1080
#define SND_DURATION 30
#define SAMPLE_RATE 48000
#define SND_NUMCHANNELS 2
#define SND_NUMSAMPLES (SND_DURATION*SAMPLE_RATE)
#define SND_NUMSAMPLESC (SND_NUMSAMPLES*SND_NUMCHANNELS)
float samples[SND_NUMSAMPLESC];
void entrypoint(){
DEVMODE dmScreenSettings={{0},0,0,sizeof(DEVMODE),0,DM_PELSWIDTH|DM_PELSHEIGHT,{0},0,0,0,0,0,{0},0,0,XRES,YRES,{0},0};
ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN);
HWND hWnd = CreateWindow((LPCSTR)0xC018,0,WS_POPUP|WS_VISIBLE|WS_MAXIMIZE,0,0,0,0,0,0,0,0);
ShowCursor(0);
HDC hDC = GetDC(hWnd);
PIXELFORMATDESCRIPTOR pfd={0,1,PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER,32,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0};
SetPixelFormat(hDC, ChoosePixelFormat(hDC, &pfd) , &pfd);
HGLRC hGLrc = wglCreateContext(hDC);
wglMakeCurrent(hDC, hGLrc);
GLuint tmp;
GLuint programMzk = ((PFNGLCREATEPROGRAMPROC)wglGetProcAddress("glCreateProgram"))();
tmp = ((PFNGLCREATESHADERPROC)wglGetProcAddress("glCreateShader"))(GL_VERTEX_SHADER);
((PFNGLSHADERSOURCEPROC)wglGetProcAddress("glShaderSource"))(tmp, 1, &msh, 0);
((PFNGLCOMPILESHADERPROC)wglGetProcAddress("glCompileShader"))(tmp);
((PFNGLATTACHSHADERPROC)wglGetProcAddress("glAttachShader"))(programMzk, tmp);
const GLchar* outs[] = {O_GAIN};
((PFNGLTRANSFORMFEEDBACKVARYINGSPROC)wglGetProcAddress("glTransformFeedbackVaryings"))(programMzk, 1, outs, GL_INTERLEAVED_ATTRIBS);
((PFNGLLINKPROGRAMPROC)wglGetProcAddress("glLinkProgram"))(programMzk);
((PFNGLUSEPROGRAMPROC)wglGetProcAddress("glUseProgram"))(programMzk);
((PFNGLGENBUFFERSPROC)wglGetProcAddress("glGenBuffers"))(1, &tmp);
((PFNGLBINDBUFFERPROC)wglGetProcAddress("glBindBuffer"))(GL_ARRAY_BUFFER, tmp);
((PFNGLBUFFERDATAPROC)wglGetProcAddress("glBufferData"))(GL_ARRAY_BUFFER, SND_NUMSAMPLESC*sizeof(float), 0, GL_STATIC_READ);
((PFNGLBINDBUFFERBASEPROC)wglGetProcAddress("glBindBufferBase"))(GL_TRANSFORM_FEEDBACK_BUFFER, 0, tmp);
((PFNGLUNIFORM1FPROC)wglGetProcAddress("glUniform1f"))(
((PFNGLGETUNIFORMLOCATIONPROC)wglGetProcAddress("glGetUniformLocation"))(programMzk, U_SAMPLERATE),
(float)SAMPLE_RATE
);
glEnable(GL_RASTERIZER_DISCARD);
((PFNGLBEGINTRANSFORMFEEDBACKPROC)wglGetProcAddress("glBeginTransformFeedback"))(GL_POINTS);
glDrawArrays(GL_POINTS, 0, SND_NUMSAMPLES);
((PFNGLENDTRANSFORMFEEDBACKPROC)wglGetProcAddress("glEndTransformFeedback"))();
glDisable(GL_RASTERIZER_DISCARD);
((PFNGLGETBUFFERSUBDATAPROC)wglGetProcAddress("glGetBufferSubData"))(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(samples), samples);
GLint program = ((PFNGLCREATEPROGRAMPROC)wglGetProcAddress("glCreateProgram"))();
tmp = ((PFNGLCREATESHADERPROC)wglGetProcAddress("glCreateShader"))(GL_VERTEX_SHADER);
((PFNGLSHADERSOURCEPROC)wglGetProcAddress("glShaderSource"))(tmp, 1, &vsh, 0);
((PFNGLCOMPILESHADERPROC)wglGetProcAddress("glCompileShader"))(tmp);
((PFNGLATTACHSHADERPROC)wglGetProcAddress("glAttachShader"))(program, tmp);
tmp = ((PFNGLCREATESHADERPROC)wglGetProcAddress("glCreateShader"))(GL_FRAGMENT_SHADER);
((PFNGLSHADERSOURCEPROC)wglGetProcAddress("glShaderSource"))(tmp, 1, &fsh, 0);
((PFNGLCOMPILESHADERPROC)wglGetProcAddress("glCompileShader"))(tmp);
((PFNGLATTACHSHADERPROC)wglGetProcAddress("glAttachShader"))(program, tmp);
((PFNGLLINKPROGRAMPROC)wglGetProcAddress("glLinkProgram"))(program);
((PFNGLUSEPROGRAMPROC)wglGetProcAddress("glUseProgram"))(program);
((PFNGLUNIFORM2FPROC)wglGetProcAddress("glUniform2f"))(
((PFNGLGETUNIFORMLOCATIONPROC)wglGetProcAddress("glGetUniformLocation"))(program, U_RESOLUTION),
(float)XRES,
(float)YRES
);
WAVEFORMATEX wave_format = {
WAVE_FORMAT_IEEE_FLOAT,
SND_NUMCHANNELS,
SAMPLE_RATE,
SAMPLE_RATE*sizeof(float)*SND_NUMCHANNELS,
sizeof(float)*SND_NUMCHANNELS,
sizeof(float)*8,
0
};
WAVEHDR wave_hdr = {(LPSTR)samples, sizeof(samples)};
HWAVEOUT hWaveOut;
waveOutOpen(&hWaveOut, WAVE_MAPPER, &wave_format, (DWORD_PTR)hWnd, 0, CALLBACK_WINDOW);
waveOutPrepareHeader(hWaveOut, &wave_hdr, sizeof(wave_hdr));
waveOutWrite(hWaveOut, &wave_hdr, sizeof(wave_hdr));
MMTIME mmt = { TIME_SAMPLES };
MSG msg;
do {
PeekMessage(&msg, 0, 0, 0, TRUE);
waveOutGetPosition(hWaveOut, &mmt, sizeof(mmt));
float time = (float)mmt.u.sample / (float)SAMPLE_RATE;
((PFNGLUNIFORM1FPROC)wglGetProcAddress("glUniform1f"))(
((PFNGLGETUNIFORMLOCATIONPROC)wglGetProcAddress("glGetUniformLocation"))(program, U_TIME),
time
);
glRects(1, 1, -1, -1);
SwapBuffers(hDC);
} while (!GetAsyncKeyState(VK_ESCAPE) && (mmt.u.sample < SND_NUMSAMPLES));
ExitProcess(0);
}
そんなに長いものじゃありません。このshaderの書かれている部分を書き換えれば、オリジナルの4K introです。
作る為の下準備
c言語をコンパイルする為にgccを導入します。
crinkler.exeをダウンロードしてきます。http://crinkler.net/
32bit版のGdi32.Lib, GlU32.Lib, kernel32.Lib, OpenGL32.Lib, User32.Lib, WinMM.Libを手に入れます。
これらのlibファイルは私の場合windowsのフォルダ内を検索したら入っていたので、それを使ってます。
なので入手方法がよくわかりません。自力で手にいれてください。
もし、わかる方がいましたらコメントをもらえるとありがたいです。
gccの導入
これが、ちょっと厄介でありまして。 crinkler.exeは、32bitのオブジェファイルしか受け付けてくれません。
なのでMinGw(32bit)のgccにすればいいのですが、glext.hが壊れているみたい。 差し替えれば使えますが、ちょっと面倒です。他にも面倒な事もあります。
なので私はmingw-w64のgccに-m32のオプションをつけて使ってます。
http://mingw-w64.org/doku.php
追記:crinkler.exeは、32bitのオブジェファイルしか受け付けてくれない。について後から気付いたことだが、そもそも実行ファイルの大きさに制限があるintroで64bitを使えば単純に実行ファイルが倍になるのだから採用されないでしょう。そういう事だな。
では、作ってみます
わかり易くする為に一つのフォルダに必要なものを全部入れちゃいます。
流れが理解できたらパスを設定するなり必要なファイルを管理してください。
冒頭に載せたC言語のソースをdemo.cのファイルとして置いてください。
crinkler.exe
Gdi32.Lib, GlU32.Lib, kernel32.Lib, OpenGL32.Lib, User32.Lib, WinMM.Lib
これも同じフォルダに入れてください。
echo off
set PATH=C:\mingw-w64\x86_64-6.1.0-posix-seh-rt_v5-rev0\mingw64\bin;%PATH%
gcc -m32 -c demo.c -o demo.o
crinkler /OUT:demo.exe /LIBPATH:./ /COMPMODE:SLOW /ENTRY:entrypoint /RANGE:opengl32 /SUBSYSTEM:WINDOWS /ORDERTRIES:1000 /PRINT:IMPORTS /PRINT:LABELS kernel32.lib user32.lib gdi32.lib opengl32.lib winmm.lib demo.o
demo.exe
exit
これを'demo_make.bat'とか適当な名前を付けて同じフォルダに入れてください。
この2行目は自分のmingw64の環境にあわせて書き換えてください。
そしたら、このbatファイルを実行してください。
しまらく待てば、コンパイルしてデモが起動します。
これで出来上がり。
ESCキーでデモは終了します。
crinkler.exeのオプションの全てを把握しているわけではありませんが、
/LIBPATH:./ について
kernel32.lib等のライブラリーファイルを他のフォルダに移動したら、ここを変更してください。
この場合はバッチファイルを起動したフォルダになってます。
/COMPMODE:SLOW について
コンパイルモードがSLOWになってますがFASTってのもあります。
後は変える事も無いように思えます。必要なら自力でやってください。
C言語のマクロ部分の説明
#define U_RESOLUTION "resolution"
#define U_SAMPLERATE "sampleRate"
#define U_TIME "time"
#define O_GAIN "gain"
#define XRES 1920
#define YRES 1080
#define SND_DURATION 30
#define SAMPLE_RATE 48000
#define SND_NUMCHANNELS 2
shaderのuniform名、out名とC言語のuniform名、out名を繋ぐ為に、U_RESOLUTION,U_SAMPLERATE ,U_TIME, O_GAIN を用意しました。shaderのuniform名、out名を変えた時は、ここも書き直してください。
XRES,YRES は画面サイズです。今の数字はTDFのデフォルトになっています。
SND_DURATION は音楽の演奏時間(秒)になっています。それと同時にデモの上映時間と一緒になっています。
SAMPLE_RATE は音のサンプリングレート。
SND_NUMCHANNELS は音のチャンネル数です。
shaderのミニ化処理
4K intro とは実行ファイルサイズが4096バイトを超えないこと、と言う条件があります。
今これをコンパイルすれば1.43 KBで条件を満たしてますが、shaderの記述量が増えるとshaderのミニ化処理が必要になります。
ミニ化とは
float hoge = hoge2 + hoge3;
を
float a=b+c;
みたいに記述量を減らす作業。
これを自分でするとなると、困難が予想されます。でもミニ化用exeが存在しています。
shader_minifier.exe http://www.ctrl-alt-test.fr/?page_id=7
これはshaderのtextファイル名を入力するとC言語のヘッターファイルを出力してくれます。
これをC言語にファイルに絡めてコンパイルします。
用法は自力で見つけてください。このミニ化周辺のスキルゲットが4K introの醍醐味です。
もう一つ
GLSL Compiler - Minifier for GLSL.
http://glslunit.appspot.com/compiler.html
というオンラインでミニ化処理の出来るページもあります。
私の場合は自分が書いたshaderとshader_minifier.exeの相性がわるくて、pythonでミニ化処理のスクリプトを書きました。
この部分はshaderを書くのとは、また違った楽しみがあります。こういうのをハックっていうんですかね?
無理に4kにこだわる事はない
TDFにおいて4k introを出品したが自分でもクオリティの低さに悲しくなしました。
サイズが先じゃなくてクオリティが先でした。8Kとか64Kとかのカテゴリーあるので、緩く行きましょう。64Kなんかはミニ化処理しなくてもサイズが余っちゃうんじゃないですかね。
とりあえず映像だけでいいやって緩いのも有りかな?!
そんな時は、これでいいんじゃないかな?
const char *msh = \
"#version 330\n"
"out vec2 gain;"
"uniform float sampleRate;"
"void main()"
"{"
"gain = vec2(0);"
"}";
おわりに
去年、情報が余り無い中なんとか実行ファイル制作までこぎつけました。
わかってみれは実行ファイル制作自体は簡単でした。
問題は、ファイルのサイズ制限です。この4Kというバランスが絶妙なところなのかと思いました。
ここのチューニングがデモの魅力なのですかね?
このあたりは企業秘密(笑)にあたるとこだと思うので楽しみは奪わないでおきます。
と言うか単なる手抜きです。
C言語のopenGL部分を書き直したい事ができてもwebGLと手順は変わらないので、だいたい検討がつくのじゃないですかね。適当に書き直して使ってください。
参考までに
詳しい境界はわかりませんがopenGLにshaderが採用される以前の関数名は
glEnable()
みたいにわかりやすいがshaderが採用された以降の関数名は
((PFNGLCREATESHADERPROC)wglGetProcAddress("glCreateShader"))()
みたいになってます。
この辺りの仕組みとかは、あまり気にせずにそんなものかで使った方が良いと思います。
この場合だと
"glCreateShader"の文字を大文字に変換した"GLCREATESHADER"の前に"PFN"後に"PROC"を付けた文字"PFNGLCREATESHADERPROC"と"wglGetProcAddress"(この文字は全関数共通)と"glCreateShader"の組み合わせで関数名が出来てます。冗長ですが簡単なルールです。
C言語の改行あり文字列の書き方は
static const char *vsh = \
"void main()"
"{"
"gl_Position = gl_Vertex;"
"}";
みたいになってます。
これは、面倒なのでpython等を使って加工するか、
static const char *vsh = "void main(){gl_Position = gl_Vertex;}";
と1行に書いてしまうのが良いと思います。
このソースはC言語で書かれています。C++ではありません。
C++にしてしまうとコンパイルできない事が発生します。CソースをいじるならC言語にしてください。
それでもC++にしたいなら、自力でお願いします。更にC++はよくわかりません。
参考情報として、細かい所は忘れてしまったのですが、C++にするならgccの場合void entrypoint()の前にextern“C”が必要だった気がします。