【4kb】Visual Studio 2017で小さいexeを作る【メガデモ】

Cluster,Inc. Advent Calendar 2018、12日目の記事です。 qiita.com

以前書いたこちらの記事のリライトになります。 wordpress.notargs.com

概要

プログラムで描いた映像、音楽を前のスクリーンにデカデカと写し、その年最強のデモシーナー(プログラムでエモいの作る人)を決める「TokyoDemoFest」というイベントがあります。

tokyodemofest.jp

自分も毎年参加しているのですが、「吐き出されるexeが小さければ小さいほど評価が高い」という傾向があるので、最新のVisualStudioで小さいexeを吐き出す方法を調べてみました。

作ったもの

TokyoDemoFestのトップページ用に書いたOptical StreamというシェーダーをMinifyしてexeに固めてみたところ、1.64kbになりました。

Shadertoy BETA

f:id:notargs:20181211132602p:plain

f:id:notargs:20181211131321p:plain

どうやっているのか

LinkerにCrinkler、シェーダーのMinifyにShader Minifierを使い、OpenGLで描画します。

The Crinkler executable file compressor

Shader Minifier - Ctrl-Alt-TestCtrl-Alt-Test

また、すべての既定のライブラリの無視はい(/NODEFAULTLIB)に設定し、不要なライブラリがリンクされないようにします。

f:id:notargs:20181211134547p:plain

設定する箇所

空のプロジェクトから新規プロジェクトを作成し、下記の設定をしていきます。 x64はcrinklerが正しく動かないためスルーし、x86のみ設定していきます。

f:id:notargs:20181211134704p:plain

Release/Debug共通

  • 構成プロパティ/全般/文字セットマルチバイト文字セットを使用するに変更します。
  • 構成プロパティ/C/C++/追加のインクルードディレクトリ$(ProjectDir)を設定します。
  • 構成プロパティ/リンカー/入力/追加の依存ファイルopengl32.lib;%(AdditionalDependencies)を設定します。

Debugのみ

  • 構成プロパティ/リンカー/システム/サブシステムコンソール(/SUBSYSTEM:CONSOLE)を設定します。

Releaseのみ

  • 構成プロパティ/全般/プログラム全体の最適化を、プログラム全体の最適化なしに設定します。
  • 構成プロパティ/VC++ディレクトリ/実行可能ファイルディレクトリ$(SolutionDir);$(ExecutablePath)を設定します。
  • 構成プロパティ/リンカー/入力/すべての既定のライブラリの無視はい (/NODEFAULTLIB)を設定します。
  • 構成プロパティ/リンカー/システム/サブシステムWindows(/SUBSYSTEM:WINDOWS)を設定します。
  • 構成プロパティ/リンカー/コマンドライン/CRINKLERを設定します。

Crinklerの配置

Crinklerを下記のページから落としてきてlink.exeにリネームし、ソリューションディレクトリに配置します。

The Crinkler executable file compressor

f:id:notargs:20181211135646p:plain

シェーダーのコンパイル

まず適当なフラグメントシェーダー、頂点シェーダーを用意します。 ここでは、Optical Streamを適当にコンバートしたものを使います。

Fragment.glsl

#version 120

varying vec2 vF;
uniform float iTime = 0.0;
vec2 iResolution = vec2(1920.0, 1080.0);


const float PI = 3.1415926;

vec3 rgb2hsv(vec3 hsv)
{
    vec4 t = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(vec3(hsv.x) + t.xyz) * 6.0 - vec3(t.w));
    return hsv.z * mix(vec3(t.x), clamp(p - vec3(t.x), 0.0, 1.0), hsv.y);
}

mat2 rotate(float a)
{
    float s = sin(a);
    float c = cos(a);
    return mat2(
        c, -s,
        s, c
    );
}

float rand(vec4 co)
{
    return fract(sin(dot(co, vec4(12.9898, 78.233, 15.2358, 29.23851))) * 43758.5453);
}

float groundDist(vec3 pos)
{
    pos.y += sin(pos.z * 0.2 + pos.x + iTime * 10.0) * 0.5;
    pos.x = mod(pos.x, 4.0) - 2.0;
    return length(pos.yx);
}

float particleDist(vec3 pos)
{
    pos += cross(sin(pos * 0.05 + iTime), cos(pos * 0.05 + iTime)) * 3.0;
    pos.z += iTime * 200.0;
    vec3 id = floor(pos / 16.0);
    pos = mod(pos, 16.0) - 8.0;
    pos += vec3(rand(vec4(id, 0.0)), rand(vec4(id, 1.0)), rand(vec4(id, 2.0))) * 10.0 - 5.0;
    return max(length(pos.yx), abs(pos.z) - 2.0);
}

float skyDist(vec3 pos)
{
    pos.z += iTime * 50.0;
    vec3 id = floor(pos / 50.0);
    
    vec3 t = iTime * vec3(0.0125, 0.25, 0.5);
    vec3 a = vec3(rand(vec4(id, floor(t.x))), rand(vec4(id + 10.0, floor(t.y))), rand(vec4(id + 20.0, floor(t.z))));
    vec3 b = vec3(rand(vec4(id, floor(t.x + 1.0))), rand(vec4(id + 10.0, floor(t.y + 1.0))), rand(vec4(id + 20.0, floor(t.z + 1.0))));
    vec3 c = mix(a, b, pow(fract(t), vec3(1.0 / 4.0)));
    
    float s = sign(mod(id.x + id.y + id.z + 0.5, 2.0) - 1.0);
    vec3 u = iTime / 3.0 + vec3(1.0, 2.0, 3.0) / 3.0;
    vec3 d = floor(u);
    vec3 e = floor(u + 1.0);
    vec3 f = mix(d, e, pow(fract(u), vec3(1.0 / 8.0)));
    
    pos = mod(pos, 50.0) - 25.0;
    for (int i = 0; i < 3; ++i)
    {
        pos.yz = rotate(f.x * PI / 2.0 * s) * pos.yz;
        pos.xz = rotate(f.y * PI / 2.0 * s) * pos.xz;
        pos.xy = rotate(f.z * PI / 2.0 * s) * pos.xy;
        pos = abs(pos);
        pos -= (c * 12.0);
        pos *= 2.0;
        if (pos.x > pos.z) pos.xz = pos.zx;
        if (pos.y > pos.z) pos.yz = pos.zy;
        if (pos.x < pos.y) pos.xy = pos.yx;
    }
    return length(pos.xz) / 8.0;
}

float dist(vec3 pos)
{
    float d = 3.402823466E+38;
    d = min(d, groundDist(pos));
    d = min(d, skyDist(pos));
    return d;
}

vec3 calcNormal(vec3 pos)
{
    vec2 ep = vec2(0.001, 0.0);
    return normalize(vec3(
        dist(pos + ep.xyy) - dist(pos - ep.xyy),
        dist(pos + ep.yxy) - dist(pos - ep.yxy),
        dist(pos + ep.yyx) - dist(pos - ep.yyx)
    ));
}

vec3 calcColor(vec3 pos)
{
    return rgb2hsv(vec3(pos.x * 0.04 + iTime, 1, 1));
}

vec3 march(vec3 pos, vec3 dir)
{
    vec3 color = vec3(0.0, 0.0, 0.0);
    for (int i = 0; i < 32; ++i)
    {
        float d = dist(pos);
        pos += dir * d * 0.9;
        color += max(vec3(0.0), 0.02 / d * calcColor(pos));
    }
    
    return color;
}

vec3 marchParticle(vec3 pos, vec3 dir)
{
    vec3 color = vec3(0.0, 0.0, 0.0);
    for (int i = 0; i < 32; ++i)
    {
        float d = particleDist(pos);
        pos += dir * d * 0.9;
        color += max(vec3(0.0), 0.005 / d * vec3(1.0, 1.0, 1.0));
    }
    
    return color;
}


void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 p = (fragCoord.xy * 2.0 - iResolution.xy) / iResolution.y;
    
    vec3 pos = vec3(0, 0.0, -10);
    vec3 dir = normalize(vec3(p, 1.0));
    dir.yz = rotate(-0.5) * dir.yz;
    pos.yz = rotate(-0.5) * pos.yz;
    dir.xz = rotate(sin(iTime) * 0.3) * dir.xz;
    pos.xz = rotate(sin(iTime) * 0.3) * pos.xz;
    dir.xy = rotate(0.1 + sin(iTime * 0.7) * 0.1) * dir.xy;
    pos.xy = rotate(0.1 + sin(iTime * 0.7) * 0.1) * pos.xy;
    
    vec3 color = vec3(0, 0, 0) * length(p.xy) * sin(iTime * 10.0);
    
    color += march(pos, dir);
    color += marchParticle(pos, dir);
    
    fragColor = vec4(color, 1.0);
}
void main(void)
{
    mainImage(gl_FragColor, vF);
}

Vertex.glsl

#version 120

varying vec2 vF;

void main(){
    gl_Position = gl_Vertex;
    vF = (gl_Vertex.xy + 1) / 2.0 * vec2(1920, 1080);
}

下記のページからShader Minifierをダウンロードし、シェーダーと同じディレクトリに配置します。

Shader Minifier - Ctrl-Alt-TestCtrl-Alt-Test

Shader Minifierを実行するbatファイル

下記のバッチファイルを作り、Shader Minifier、各種シェーダーのあるディレクトリに配置します。

cd /d %~dp0

shader_minifier.exe Fragment.glsl --preserve-externals -o Fragment.h
shader_minifier.exe Vertex.glsl --preserve-externals -o Vertex.h

実行すると、Vertex.hFragment.hというファイルが出力されたことがわかります。

f:id:notargs:20181211140038p:plain

出力されたFragment.h

/* File generated with Shader Minifier 1.1.5
 * http://www.ctrl-alt-test.fr
 */
#ifndef FRAGMENT_H_
# define FRAGMENT_H_

const char *Fragment_glsl =
 "#version 120\n"
 "varying vec2 vF;"
 "uniform float iTime=0.;"
 "vec2 z=vec2(1920.,1080.);"
 "const float v=3.14159;"
 "vec3 n(vec3 v)"
 "{"
   "vec4 i=vec4(1.,2./3.,1./3.,3.);"
   "vec3 f=abs(fract(vec3(v.x)+i.xyz)*6.-vec3(i.w));"
   "return v.z*mix(vec3(i.x),clamp(f-vec3(i.x),0.,1.),v.y);"
 "}"
 "mat2 r(float v)"
 "{"
   "float f=sin(v),i=cos(v);"
   "return mat2(i,-f,f,i);"
 "}"
 "float f(vec4 v)"
 "{"
   "return fract(sin(dot(v,vec4(12.9898,78.233,15.2358,29.2385)))*43758.5);"
 "}"
 "float x(vec3 v)"
 "{"
   "return v.y+=sin(v.z*.2+v.x+iTime*10.)*.5,v.x=mod(v.x,4.)-2.,length(v.yx);"
 "}"
 "float m(vec3 v)"
 "{"
   "v+=cross(sin(v*.05+iTime),cos(v*.05+iTime))*3.;"
   "v.z+=iTime*200.;"
   "vec3 i=floor(v/16.);"
   "v=mod(v,16.)-8.;"
   "v+=vec3(f(vec4(i,0.)),f(vec4(i,1.)),f(vec4(i,2.)))*10.-5.;"
   "return max(length(v.yx),abs(v.z)-2.);"
 "}"
 "float s(vec3 i)"
 "{"
   "i.z+=iTime*50.;"
   "vec3 z=floor(i/50.),y=iTime*vec3(.0125,.25,.5),s=vec3(f(vec4(z,floor(y.x))),f(vec4(z+10.,floor(y.y))),f(vec4(z+20.,floor(y.z)))),x=vec3(f(vec4(z,floor(y.x+1.))),f(vec4(z+10.,floor(y.y+1.))),f(vec4(z+20.,floor(y.z+1.)))),m=mix(s,x,pow(fract(y),vec3(.25)));"
   "float e=sign(mod(z.x+z.y+z.z+.5,2.)-1.);"
   "vec3 n=iTime/3.+vec3(1.,2.,3.)/3.,c=floor(n),p=floor(n+1.),l=mix(c,p,pow(fract(n),vec3(.125)));"
   "i=mod(i,50.)-25.;"
   "for(int t=0;t<3;++t)"
     "{"
       "i.yz=r(l.x*v/2.*e)*i.yz;"
       "i.xz=r(l.y*v/2.*e)*i.xz;"
       "i.xy=r(l.z*v/2.*e)*i.xy;"
       "i=abs(i);"
       "i-=m*12.;"
       "i*=2.;"
       "if(i.x>i.z)"
         "i.xz=i.zx;"
       "if(i.y>i.z)"
         "i.yz=i.zy;"
       "if(i.x<i.y)"
         "i.xy=i.yx;"
     "}"
   "return length(i.xz)/8.;"
 "}"
 "float i(vec3 v)"
 "{"
   "float i=3.40282e+38;"
   "i=min(i,x(v));"
   "i=min(i,s(v));"
   "return i;"
 "}"
 "vec3 t(vec3 v)"
 "{"
   "vec2 z=vec2(.001,0.);"
   "return normalize(vec3(i(v+z.xyy)-i(v-z.xyy),i(v+z.yxy)-i(v-z.yxy),i(v+z.yyx)-i(v-z.yyx)));"
 "}"
 "vec3 d(vec3 v)"
 "{"
   "return n(vec3(v.x*.04+iTime,1,1));"
 "}"
 "vec3 d(vec3 v,vec3 m)"
 "{"
   "vec3 z=vec3(0.,0.,0.);"
   "for(int y=0;y<32;++y)"
     "{"
       "float e=i(v);"
       "v+=m*e*.9;"
       "z+=max(vec3(0.),.02/e*d(v));"
     "}"
   "return z;"
 "}"
 "vec3 f(vec3 v,vec3 i)"
 "{"
   "vec3 z=vec3(0.,0.,0.);"
   "for(int y=0;y<32;++y)"
     "{"
       "float e=m(v);"
       "v+=i*e*.9;"
       "z+=max(vec3(0.),.005/e*vec3(1.,1.,1.));"
     "}"
   "return z;"
 "}"
 "void i(out vec4 v,in vec2 i)"
 "{"
   "vec2 y=(i.xy*2.-z.xy)/z.y;"
   "vec3 e=vec3(0,0.,-10),x=normalize(vec3(y,1.));"
   "x.yz=r(-.5)*x.yz;"
   "e.yz=r(-.5)*e.yz;"
   "x.xz=r(sin(iTime)*.3)*x.xz;"
   "e.xz=r(sin(iTime)*.3)*e.xz;"
   "x.xy=r(.1+sin(iTime*.7)*.1)*x.xy;"
   "e.xy=r(.1+sin(iTime*.7)*.1)*e.xy;"
   "vec3 n=vec3(0,0,0)*length(y.xy)*sin(iTime*10.);"
   "n+=d(e,x);"
   "n+=f(e,x);"
   "v=vec4(n,1.);"
 "}"
 "void main()"
 "{"
   "i(gl_FragColor,vF);"
 "}";

#endif // FRAGMENT_H_

出力されたVertex.h

/* File generated with Shader Minifier 1.1.5
 * http://www.ctrl-alt-test.fr
 */
#ifndef VERTEX_H_
# define VERTEX_H_

const char *Vertex_glsl =
 "#version 120\n"
 "varying vec2 vF;"
 "void main()"
 "{"
   "gl_Position=gl_Vertex,vF=(gl_Vertex.xy+1)/2.*vec2(1920,1080);"
 "}";

#endif // VERTEX_H_

これらはそのままC++でincludeして使うことができます。

コードを書く

あとは適当に↑をつかって画像を描画するコードを書いて完成です。

Main.cpp

#include <windows.h>

#include <gl/GL.h>
#include "gl/glext.h"
#include "Fragment.h"
#include "Vertex.h"
#include <cstdio>

#define    SCREEN_WIDTH    1920
#define    SCREEN_HEIGHT   1080

#ifndef _DEBUG
#define FULL_SCREEN
#endif

inline bool IsNotClosed()
{
#ifndef FULL_SCREEN
    MSG msg;
    PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE);
    if (msg.message == WM_NCLBUTTONDOWN && msg.wParam == HTCLOSE)
    {
        return false;
    }
    DispatchMessage(&msg);
#endif
    return !GetAsyncKeyState(VK_ESCAPE);
}

#if _DEBUG
inline void printShaderInfoLog(GLuint shader)
{
    GLsizei bufSize;

    PFNGLGETSHADERIVPROC(wglGetProcAddress("glGetShaderiv"))(shader, GL_INFO_LOG_LENGTH, &bufSize);

    if (bufSize > 1)
    {
        const auto infoLog = static_cast<GLchar *>(malloc(bufSize));

        if (infoLog != nullptr)
        {
            GLsizei length;

            PFNGLGETSHADERINFOLOGPROC(wglGetProcAddress("glGetShaderInfoLog"))(shader, bufSize, &length, infoLog);
            fprintf(stderr, "InfoLog:\n%s\n\n", infoLog);
            free(infoLog);
        }
        else
        {
            fprintf(stderr, "Could not allocate InfoLog buffer.\n");
        }
    }
}
#endif

inline GLuint CompileShaderProgram()
{
    const auto program = PFNGLCREATEPROGRAMPROC(wglGetProcAddress("glCreateProgram"))();

    const auto vertex = PFNGLCREATESHADERPROC(wglGetProcAddress("glCreateShader"))(GL_VERTEX_SHADER);
    PFNGLSHADERSOURCEPROC(wglGetProcAddress("glShaderSource"))(vertex, 1, &Vertex_glsl, 0);
    PFNGLCOMPILESHADERPROC(wglGetProcAddress("glCompileShader"))(vertex);
#if _DEBUG
    printf("Vertex: \n");
    printShaderInfoLog(vertex);
#endif
    PFNGLATTACHSHADERPROC(wglGetProcAddress("glAttachShader"))(program, vertex);


    const auto fragment = PFNGLCREATESHADERPROC(wglGetProcAddress("glCreateShader"))(GL_FRAGMENT_SHADER);
    PFNGLSHADERSOURCEPROC(wglGetProcAddress("glShaderSource"))(fragment, 1, &Fragment_glsl, 0);
    PFNGLCOMPILESHADERPROC(wglGetProcAddress("glCompileShader"))(fragment);
#if _DEBUG
    printf("Fragment: \n");
    printShaderInfoLog(fragment);
#endif
    PFNGLATTACHSHADERPROC(wglGetProcAddress("glAttachShader"))(program, fragment);

    PFNGLLINKPROGRAMPROC(wglGetProcAddress("glLinkProgram"))(program);
    PFNGLUSEPROGRAMPROC(wglGetProcAddress("glUseProgram"))(program);

    return program;
}


inline void InitializeWindow(HDC& deviceContext, HWND& windowHandle, HGLRC& renderingContext)
{
#ifdef FULL_SCREEN
    static DEVMODE dmScreenSettings =
    {
        "", 0, 0, sizeof(dmScreenSettings), 0, DM_PELSWIDTH | DM_PELSHEIGHT,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
    };
    ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);
    windowHandle = CreateWindow("edit", nullptr, WS_POPUP | WS_VISIBLE | WS_MAXIMIZE, 0, 0, 0, 0, 0, 0, 0, 0);
    ShowCursor(0);
#else
    windowHandle = CreateWindow("edit", nullptr, WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT,
        nullptr, nullptr, nullptr, nullptr);
#endif
    deviceContext = GetDC(windowHandle);

    static const PIXELFORMATDESCRIPTOR g_pixelFormatDescriptor =
    {
        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(deviceContext, ChoosePixelFormat(deviceContext, &g_pixelFormatDescriptor), &g_pixelFormatDescriptor);
    renderingContext = wglCreateContext(deviceContext);
    wglMakeCurrent(deviceContext, renderingContext);
}

inline int run()
{
    HDC deviceContext;
    HWND windowHandle;
    HGLRC renderingContext;
    InitializeWindow(deviceContext, windowHandle, renderingContext);

    const auto program = CompileShaderProgram();

    const auto startTime = GetTickCount();


    while (IsNotClosed())
    {
        const auto timeLocation = PFNGLGETUNIFORMLOCATIONARBPROC(wglGetProcAddress("glGetUniformLocation"))(program, "iTime");
        PFNGLUNIFORM1FPROC(wglGetProcAddress("glUniform1f"))(timeLocation, (GetTickCount() - startTime) / 1000.0f );
        
        glRecti(1, 1, -1, -1);
        SwapBuffers(deviceContext);
    }

    wglMakeCurrent(nullptr, nullptr);
    wglDeleteContext(renderingContext);

    ReleaseDC(windowHandle, deviceContext);


    PostQuitMessage(0);
    ExitProcess(0);
}

#if _DEBUG
int main(void)
{
    run();
}
#else
void WinMainCRTStartup()
{
    run();
}
#endif

エラーの対処

gl/glext.hが無いよ!khr/khrplatform.hが無いよ!などと怒られるので、KhronosGropu公式ページから落としてきて入れてあげましょう。

https://www.khronos.org/registry/OpenGL/api/GL/glext.h

https://www.khronos.org/registry/EGL/api/KHR/khrplatform.h

ビルド

Releaseでビルドすると、1.64kbのexeが吐き出されます。 お疲れ様でした!

f:id:notargs:20181211132602p:plain

f:id:notargs:20181211131321p:plain