Created
April 1, 2026 12:14
-
-
Save Yuikawa-Akira/13cda96b52735d7c25204e9f6ac460fc to your computer and use it in GitHub Desktop.
M5-UniSphere-3D
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * [M5-UniSphere-3D] | |
| * * Copyright (c) 2026 Yuikawa-Akira | |
| * This software includes code from : https://github.com/K-Yama2010/3D_Sphere_Pong_Wars | |
| * Copyright (c) 2025 K-Yama2010 | |
| * * Released under the MIT License. | |
| */ | |
| #include <M5Unified.h> | |
| // ================================================================= | |
| // 設定 (Configuration) | |
| // ================================================================= | |
| const int GRID_SIZE = 32; // 球の分割数。上げるとテクスチャが滑らかになりますが重くなります | |
| const float SPHERE_RADIUS = 100.0f; // 球の半径 | |
| // 画面表示用スプライトのサイズ | |
| const int CANVAS_WIDTH = 240; | |
| const int CANVAS_HEIGHT = 240; | |
| // テクスチャ用スプライト(キャンバス)のサイズ (2:1の比率が球体に貼りやすいです) | |
| const int TEX_WIDTH = 64; | |
| const int TEX_HEIGHT = 32; | |
| // カメラ設定 | |
| const float CAMERA_Z = SPHERE_RADIUS * 3.0f; | |
| const float FOV = 220.0f; | |
| // FPS表示用 | |
| uint32_t lastTime = 0; | |
| float fps = 0; | |
| // ================================================================= | |
| // 3D演算用の構造体とヘルパー関数 | |
| // ================================================================= | |
| struct Vec3 { | |
| float x, y, z; | |
| Vec3(float x = 0, float y = 0, float z = 0) | |
| : x(x), y(y), z(z) {} | |
| float magnitude() const { | |
| return sqrt(x * x + y * y + z * z); | |
| } | |
| void normalize() { | |
| float mag = magnitude(); | |
| if (mag > 0) { | |
| x /= mag; | |
| y /= mag; | |
| z /= mag; | |
| } | |
| } | |
| Vec3 operator+(const Vec3& v) const { | |
| return Vec3(x + v.x, y + v.y, z + v.z); | |
| } | |
| Vec3 operator-(const Vec3& v) const { | |
| return Vec3(x - v.x, y - v.y, z - v.z); | |
| } | |
| Vec3 operator*(float s) const { | |
| return Vec3(x * s, y * s, z * s); | |
| } | |
| }; | |
| struct Vec2 { | |
| float x, y; | |
| }; | |
| struct Quad { | |
| int vertex_indices[4]; | |
| Vec3 center; | |
| uint16_t color; // テクスチャから取得した色を保持 | |
| }; | |
| Vec3 cross(const Vec3& a, const Vec3& b) { | |
| return Vec3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x); | |
| } | |
| float dot(const Vec3& a, const Vec3& b) { | |
| return a.x * b.x + a.y * b.y + a.z * b.z; | |
| } | |
| // ================================================================= | |
| // グローバル変数 | |
| // ================================================================= | |
| M5Canvas canvas(&M5.Display); // 画面描画用 | |
| M5Canvas texCanvas; // テクスチャ用(任意のキャンバス) | |
| std::vector<Vec3> vertices; | |
| std::vector<Quad> quads; | |
| float rotationY = 0.0f; | |
| float rotationX = 0.0f; | |
| // ================================================================= | |
| // 描画用のワークエリア(グローバルまたはstaticに配置して再利用する) | |
| // ================================================================= | |
| std::vector<Vec2> projected_vertices; | |
| std::vector<Vec3> rotated_vertices; | |
| struct ZOrder { | |
| float z; | |
| int index; | |
| }; | |
| std::vector<ZOrder> z_sorted_quads; | |
| // setup関数内で一度だけ呼んでメモリを予約しておく | |
| void reserveMemory() { | |
| size_t v_size = vertices.size(); | |
| size_t q_size = quads.size(); | |
| projected_vertices.reserve(v_size); | |
| rotated_vertices.reserve(v_size); | |
| z_sorted_quads.reserve(q_size); | |
| } | |
| // ================================================================= | |
| // テクスチャキャンバスの生成 | |
| // ================================================================= | |
| void createTextureCanvas() { | |
| texCanvas.createSprite(TEX_WIDTH, TEX_HEIGHT); | |
| // 背景を市松模様にする | |
| for (int y = 0; y < TEX_HEIGHT; y += 2) { | |
| for (int x = 0; x < TEX_WIDTH; x += 2) { | |
| uint16_t color = ((x / 2 + y / 2) % 2 == 0) ? TFT_DARKGREEN : TFT_GREEN; | |
| texCanvas.fillRect(x, y, 2, 2, color); | |
| } | |
| } | |
| // 文字を描画 | |
| texCanvas.setTextDatum(middle_center); | |
| texCanvas.setTextSize(1); | |
| texCanvas.setTextColor(TFT_WHITE); | |
| texCanvas.drawString("Hello!", TEX_WIDTH / 2, TEX_HEIGHT / 2); | |
| } | |
| // ================================================================= | |
| // UVスフィア生成と色情報のサンプリング | |
| // ================================================================= | |
| void createUVSphere() { | |
| vertices.clear(); | |
| quads.clear(); | |
| // 2:1のテクスチャ比率に合わせて、経度の分割数を緯度の2倍にします | |
| int latBands = GRID_SIZE; // 緯度(縦の分割) | |
| int lonBands = GRID_SIZE * 2; // 経度(横の分割) | |
| // 1. 頂点の生成 (地球儀の交点を計算) - 変更なし | |
| for (int lat = 0; lat <= latBands; lat++) { | |
| float theta = lat * PI / latBands; | |
| float sinTheta = sin(theta); | |
| float cosTheta = cos(theta); | |
| for (int lon = 0; lon <= lonBands; lon++) { | |
| float phi = lon * TWO_PI / lonBands; | |
| float sinPhi = sin(phi); | |
| float cosPhi = cos(phi); | |
| Vec3 v; | |
| v.x = SPHERE_RADIUS * cosPhi * sinTheta; | |
| v.y = SPHERE_RADIUS * cosTheta; | |
| v.z = SPHERE_RADIUS * sinPhi * sinTheta; | |
| vertices.push_back(v); | |
| } | |
| } | |
| // 2. クアッド(四角形ポリゴン)の生成と色取得 | |
| for (int lat = 0; lat < latBands; lat++) { | |
| for (int lon = 0; lon < lonBands; lon++) { | |
| int first = (lat * (lonBands + 1)) + lon; | |
| int second = first + lonBands + 1; | |
| Quad q; | |
| // 頂点インデックスのセット | |
| // ※1と3を入れ替えると内側はみえるようになる | |
| q.vertex_indices[0] = first; | |
| q.vertex_indices[1] = first + 1; | |
| q.vertex_indices[2] = second + 1; | |
| q.vertex_indices[3] = second; | |
| // 中心座標の計算 (Zソート用) | |
| q.center = (vertices[q.vertex_indices[0]] + vertices[q.vertex_indices[1]] + vertices[q.vertex_indices[2]] + vertices[q.vertex_indices[3]]) * 0.25f; | |
| // 3D座標からの逆算ではなく、グリッドのインデックスから直接UVを計算する | |
| // クアッドの中心の色を拾うため、インデックスに +0.5f します | |
| float u = (lon + 0.5f) / (float)lonBands; | |
| float v = (latBands - lat - 0.5f) / (float)latBands; | |
| // キャンバス上の座標に変換して色を取得 | |
| int tx = constrain((int)(u * TEX_WIDTH), 0, TEX_WIDTH - 1); | |
| int ty = constrain((int)(v * TEX_HEIGHT), 0, TEX_HEIGHT - 1); | |
| q.color = texCanvas.readPixel(tx, ty); | |
| quads.push_back(q); | |
| } | |
| } | |
| } | |
| // ================================================================= | |
| // 球体描画関数(メモリ効率化版) | |
| // ================================================================= | |
| void drawSphere() { | |
| canvas.fillSprite(TFT_BLACK); | |
| // 回転値をループ外で計算しておく | |
| float sX = sin(rotationX); | |
| float cX = cos(rotationX); | |
| float sY = sin(rotationY); | |
| float cY = cos(rotationY); | |
| // ベクタの中身をクリア | |
| projected_vertices.clear(); | |
| rotated_vertices.clear(); | |
| z_sorted_quads.clear(); | |
| // 1. 頂点の3D回転と2D投影 | |
| for (size_t i = 0; i < vertices.size(); ++i) { | |
| const Vec3& v = vertices[i]; // 参照渡しでコピーを抑制 | |
| // 事前計算した値を使って回転 | |
| float y1 = v.y * cX - v.z * sX; | |
| float z1 = v.y * sX + v.z * cX; | |
| Vec3 rv; | |
| rv.x = v.x * cY - z1 * sY; | |
| rv.y = y1; | |
| rv.z = v.x * sY + z1 * cY + CAMERA_Z; | |
| rotated_vertices.push_back(rv); | |
| if (rv.z <= 0.1f) { // ゼロ除算防止 | |
| projected_vertices.push_back({ -9999, -9999 }); | |
| } else { | |
| float scale = FOV / rv.z; | |
| projected_vertices.push_back({ rv.x * scale + CANVAS_WIDTH / 2, | |
| rv.y * scale + CANVAS_HEIGHT / 2 }); | |
| } | |
| } | |
| // 2. Zソート | |
| for (size_t i = 0; i < quads.size(); ++i) { | |
| // 回転後の座標から中心のZ値を計算 | |
| Vec3 c = quads[i].center; | |
| float y1 = c.y * cX - c.z * sX; | |
| float z1 = c.y * sX + c.z * cX; | |
| float center_z = c.x * sY + z1 * cY + CAMERA_Z; | |
| z_sorted_quads.push_back({ center_z, (int)i }); | |
| } | |
| std::sort(z_sorted_quads.begin(), z_sorted_quads.end(), [](const ZOrder& a, const ZOrder& b) { | |
| return a.z > b.z; | |
| }); | |
| // 3. 描画処理 | |
| for (const auto& q_order : z_sorted_quads) { | |
| int i = q_order.index; | |
| // 頂点取り出し | |
| Vec2 p[4]; | |
| Vec3 r[4]; | |
| bool visible = true; | |
| for (int j = 0; j < 4; ++j) { | |
| int idx = quads[i].vertex_indices[j]; | |
| p[j] = projected_vertices[idx]; | |
| r[j] = rotated_vertices[idx]; | |
| if (p[j].x == -9999) { | |
| visible = false; | |
| break; | |
| } | |
| } | |
| if (!visible) continue; | |
| // 背面カリング | |
| Vec3 edge1 = r[1] - r[0]; | |
| Vec3 edge2 = r[2] - r[0]; | |
| Vec3 normal = cross(edge1, edge2); | |
| if (dot(normal, r[0] * -1.0f) < 0) continue; | |
| // 三角形描画 | |
| canvas.fillTriangle(p[0].x, p[0].y, p[1].x, p[1].y, p[2].x, p[2].y, quads[i].color); | |
| canvas.fillTriangle(p[0].x, p[0].y, p[2].x, p[2].y, p[3].x, p[3].y, quads[i].color); | |
| } | |
| // --- ここからFPS表示 --- | |
| canvas.setTextColor(TFT_WHITE, TFT_BLACK); // 文字色:白、背景:黒 | |
| canvas.setTextDatum(top_right); // 右上基準で描画 | |
| canvas.setFont(&fonts::Font0); // 標準フォント | |
| canvas.setCursor(CANVAS_WIDTH - 5, 5); | |
| canvas.printf("FPS: %.1f", fps); | |
| // --- ここまで --- | |
| canvas.pushSprite((M5.Display.width() - CANVAS_WIDTH) / 2, (M5.Display.height() - CANVAS_HEIGHT) / 2); | |
| } | |
| // ================================================================= | |
| // セットアップ | |
| // ================================================================= | |
| void setup(void) { | |
| auto cfg = M5.config(); | |
| M5.begin(cfg); | |
| //Serial.begin(115200); | |
| canvas.createSprite(CANVAS_WIDTH, CANVAS_HEIGHT); | |
| createTextureCanvas(); | |
| createUVSphere(); | |
| reserveMemory(); | |
| } | |
| // ================================================================= | |
| // メインループ | |
| // ================================================================= | |
| void loop(void) { | |
| M5.update(); | |
| // FPSの計算 | |
| uint32_t now = millis(); | |
| uint32_t dt = now - lastTime; | |
| if (dt > 0) { | |
| // 1000ms / 経過ms = FPS | |
| // 急激な数値変化を抑えるための簡易フィルタ(補間) | |
| fps = fps * 0.9f + (1000.0f / dt) * 0.1f; | |
| } | |
| lastTime = now; | |
| // 1. 自動回転の更新 | |
| rotationY -= 0.03f; | |
| //rotationX += 0.015f; | |
| if (rotationY > TWO_PI) rotationY -= TWO_PI; | |
| if (rotationX > TWO_PI) rotationX -= TWO_PI; | |
| // 2. 描画関数の呼び出し | |
| drawSphere(); | |
| yield(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment