2026年2月24日 星期二

用 emcc compiler 寫簡單的 openGL 繪圖, 讓瀏覽器也能觀看

在瀏覽開啟 html 檔, 裡面除了可以用 javascript 語言來運行openGL ES, 也能用 wasm 語言來運作 , 透過 emcc 編譯器可以將 c 語言翻譯成 wasm, 安裝方式詳如 Emscripten 官網:

https://emscripten.org/docs/getting_started/downloads.html

在 linux 系統上, 我將 emsdk 安裝到 ~/project/emsdk 目錄內, 底下是簡單的 Makefile 用來將檔案輸出到 ramdisk (/dev/shm)內, 只要執行 make 就能產生包含用 g++ 編譯的可執行檔 /dev/shm/main.out,  加上可以讓瀏覽器開啟的網頁(/dev/shm/main.htm) 和 javascript 執行檔 (/dev/shm/main_wasm.js)

#Makefile
# := 變數指定一次
#  = 變數可以重複指定
# 所有來源     : $^
# 第1來源      : $<
# 目標主名+副名: $@
# 目標主名     : $*

c_SRC   := gltest.cpp
js_HTML := main.htm
js_WASM := main_wasm.js
path_DST:= /dev/shm
path_SRC:= $(shell pwd)
cc_LIBs := -I include -l GL -l glut
em_LIBs := -I include -s WASM=1 -s LEGACY_GL_EMULATION=1 -s USE_WEBGL2=1 -s SINGLE_FILE -s USE_FREETYPE=1
rd_HTML := $(path_DST)/$(js_HTML)
rd_WASM := $(path_DST)/$(js_WASM)
rd_EXE  := $(path_DST)/main.out
define html_content
    <!DOCTYPE html>
    <html><head><meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>
        <center>
            <canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
            <script type="text/javascript">var id_from_canvas = document.getElementById("canvas");var Module = {canvas: id_from_canvas};</script>
            <script src="$(js_WASM)"></script>
        </center>
    </body></html>
endef

all: $(rd_HTML) $(rd_WASM) $(rd_EXE)
    @echo open $< to run $(rd_WASM) in browser
    
run: $(rd_EXE)
    $(rd_EXE)

$(rd_WASM): $(c_SRC)
    cd ~/project/emsdk && . emsdk_env.sh && cd $(path_SRC) && emcc $^ $(em_LIBs)  -o $@ && echo " "

$(rd_HTML):
    $(file > $@, $(html_content))

$(rd_EXE):$(c_SRC)
    @g++ $^ $(cc_LIBs)  -lfreetype -o $@

clean:
    rm -f $(rd_WASM) $(rd_HTML) $(rd_EXE)

簡單用 c++ 寫一個繪圖程式 :

// gltest.cpp:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <unistd.h>
#include <GL/glut.h>
struct ColorRGB { float r,g,b; };
const ColorRGB colors[] = {
    {.r=1, .g=1, .b=1},
    {.r=1, .g=1, .b=0},
    {.r=1, .g=0, .b=1},
    {.r=1, .g=0, .b=0},
    {.r=0, .g=1, .b=1},
    {.r=0, .g=1, .b=0},
    {.r=0, .g=0, .b=1},
    {.r=0.5, .g=0.5, .b=0.5}
};
int size_n = sizeof(colors)/sizeof(colors[0]);

void draw_circle (float cx, float cy, float radius, ColorRGB c = colors[0], int max_segments = 32) {
    const double d_theta = M_PI * 2 / max_segments;
    double theta = 0;// initial θ
    int segments = max_segments;// lines to draw
    glColor3f(c.r, c.g, c.b);
    glBegin(GL_LINE_LOOP);// GL_TRIANGLE_FAN or GL_LINE_LOOP, to close loop
    while (segments -- > 0) {
        glVertex2f(cx + radius * cos(theta), cy + radius * sin(theta));
        theta += d_theta;            
    }
    glEnd();
}
void draw_line(float x0, float y0, float x1, float y1, ColorRGB c = colors[0]){
    glColor3f(c.r, c.g, c.b);
    glBegin(GL_LINES) ; // to draw one line
    glVertex2f(x0, y0); // first point
    glVertex2f(x1, y1); // second point
    glEnd();    // end drawing
}
void update_loop(int parameter){
    printf("parameter = %8d\n", parameter);
    glutPostRedisplay();// send event to redraw
    glutTimerFunc(1000, update_loop, parameter + 1);// continue to run update_loop again after 1 second
}
int main(int argc, char** argv) {   
    glutInit(&argc, argv); // Initialize GLUT
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); // Set display mode
    glutInitWindowSize(800, 600); // Set window size
    glutCreateWindow("GLUT Example"); // Create window
    atexit([]() {
        printf("size_n = %-8d, bye~bye.\n", size_n);
    });
    glutDisplayFunc([]() {// when redraw event happens
        static int current = 0;
        current = (current + rand()) % size_n;
        glClear(GL_COLOR_BUFFER_BIT);
        glColor3f(0, 0, 0);
        draw_circle( 0,  0, 0.5,     colors[current]);
        draw_line  (-1,  1,   1, -1, colors[(current + 1) % size_n]);
        draw_line  (-1, -1,   1,  1, colors[(current + 2) % size_n]);
        glFlush(); // Flush drawing command buffer. If using double buffering (GLUT_DOUBLE), use glutSwapBuffers();
    }); // Register display callback
    update_loop(0); // begin to send redraw event
    glutMainLoop(); // Enter GLUT event processing loop
    return 0;
}

後記: 如果 make 運行時出現錯誤, 有可能是 Makefile 內執行命令前面的縮排字元(\t: Tab 按鍵)被空白字元(' ': Space 按鍵)取代了, 只要用編輯器將它修正回縮排字元就能正常運作了.


用 emcc compiler 寫簡單的 openGL 繪圖, 讓瀏覽器也能觀看

在瀏覽開啟 html 檔, 裡面除了可以用 javascript 語言來運行openGL ES, 也能用 wasm 語言來運作 , 透過 emcc 編譯器可以將 c 語言翻譯成 wasm, 安裝方式詳如 Emscripten 官網: https://emscripten.org/...