Skip to content

Instantly share code, notes, and snippets.

@Yuikawa-Akira
Created April 1, 2026 12:14
Show Gist options
  • Select an option

  • Save Yuikawa-Akira/13cda96b52735d7c25204e9f6ac460fc to your computer and use it in GitHub Desktop.

Select an option

Save Yuikawa-Akira/13cda96b52735d7c25204e9f6ac460fc to your computer and use it in GitHub Desktop.
M5-UniSphere-3D
/*
* [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