点群の最小球を求めるのにWelzlのアルゴリズムというのがあるのを知った。
とりあえずコピペで動くことを確認したのでここに置く。何をやっているのかわからないのでこれから調べていく。
あと、そのままだと再帰が激しすぎてたった5000頂点でオーバーフローした。スタックサイズを変えればいけるのだろうがそういう問題ではない。
#include <iostream> #include <unordered_map> #include <set> #pragma warning(disable:4996) #include <vtkUnsignedCharArray.h> #include <vtkPointData.h> #include <vtkActor.h> #include <vtkRenderer.h> #include <vtkRenderWindow.h> #include <vtkRenderWindowInteractor.h> #include <vtkProperty.h> #include <vtkSphere.h> #include <vtkVertexGlyphFilter.h> //球とその表示に必要 #include <vtkSphereSource.h> #include <vtkPolyDataMapper.h> //VTK_MODULE_INITに必要 #include <vtkAutoInit.h> //必須 VTK_MODULE_INIT(vtkRenderingOpenGL2); VTK_MODULE_INIT(vtkInteractionStyle); #pragma comment(lib, "ws2_32.lib") #pragma comment(lib, "Psapi.lib") #pragma comment(lib, "Dbghelp.lib") #include <Eigen/Dense> #include <random> #pragma comment(lib,"opengl32.lib") // 点群型 struct MyCloud { std::vector<Eigen::Vector3d> points; std::vector<Eigen::Vector3d> colors; }; namespace welzl {
// 球を表す struct Sphere { Eigen::Vector3d center; double radius; Sphere() : center(Eigen::Vector3d::Zero()), radius(0.0) {} Sphere(const Eigen::Vector3d& c, double r) : center(c), radius(r) {} };
//------------------------------------- // R(最大4点)から決まる球を求める補助関数群 //-------------------------------------
// 1点からなる球(その点が中心、半径0) Sphere ball_from_one_point(const Eigen::Vector3d& a) { return Sphere(a, 0.0); }
// 2点からなる最小球(2点の中点が中心、半径は両点から中点までの距離) Sphere ball_from_two_points(const Eigen::Vector3d& a, const Eigen::Vector3d& b) { Eigen::Vector3d c = 0.5 * (a + b); double r = (a - c).norm(); return Sphere(c, r); }
// 3点から決まる球(3点が共通で外接する円=2次元平面での外接円を3D空間で表現) // 外接円の中心は3点からなる三角形の外心 Sphere ball_from_three_points(const Eigen::Vector3d& a, const Eigen::Vector3d& b, const Eigen::Vector3d& c) { // 2次元外心計算を3次元対応。3点は同一平面上にあるため、 // 外心は、(b - a), (c - a)に直交する2本の垂直二等分線の交点。 Eigen::Vector3d ab = b - a; Eigen::Vector3d ac = c - a; Eigen::Matrix3d M; M << ab.x(), ab.y(), ab.z(), ac.x(), ac.y(), ac.z(), ab.cross(ac).x(), ab.cross(ac).y(), ab.cross(ac).z(); // 上記はそのままでは重複があり、もう少し安定的な方法を取る必要がある。 // 簡易的には、外心は以下の方程式で求められる: // |x - a|^2 = |x - b|^2 = |x - c|^2 を解く。 // この方程式は線形代数で解ける。 // 以下では直接外心を求める一般的な手続きの1つを示す。 // 行列方程式で外心を求めるための設定 // (参考実装) 外心は幾何的には以下で求まる: // Let A, B, C be points. 外心Oは: // solve for O: // (O - A)・(B - A) = (|B-A|^2)/2 // (O - A)・(C - A) = (|C-A|^2)/2 // この2式からOが求まる。(O - A)を未知変数にした線形方程式となる Eigen::Matrix2d A_mat; A_mat << ab.dot(ab), ab.dot(ac), ab.dot(ac), ac.dot(ac); Eigen::Vector2d rhs; rhs << 0.5 * ab.dot(ab), 0.5 * ac.dot(ac); // 解く Eigen::Vector2d uv = A_mat.colPivHouseholderQr().solve(rhs); Eigen::Vector3d O = a + uv[0] * ab + uv[1] * ac; double r = (O - a).norm(); return Sphere(O, r); }
// 4点から決まる球(4点が共通球面上にある場合、その外接球を求める) // 4点外接球計算は3x3の線形方程式を解くことで可能。 Sphere ball_from_four_points(const Eigen::Vector3d& a, const Eigen::Vector3d& b, const Eigen::Vector3d& c, const Eigen::Vector3d& d) { // 方針: // (O - a)・(O - a) = (O - b)・(O - b) // (O - a)・(O - a) = (O - c)・(O - c) // (O - a)・(O - a) = (O - d)・(O - d) // という3つの方程式を立て、Oを求める。 Eigen::Vector3d A = a; Eigen::Vector3d B = b; Eigen::Vector3d C = c; Eigen::Vector3d D = d; // 方程式組み立て // (O - A)² = (O - B)² などは展開すると // O² - 2O·A + A² = O² - 2O·B + B² → 2O·(B - A) = B² - A² // 同様にC, Dについても同様の式を立てられる // よって: // 2(B - A)·O = B² - A² // 2(C - A)·O = C² - A² // 2(D - A)·O = D² - A² // ベクトルX = Oについて Eigen::Matrix3d M; M << 2 * (b.x() - a.x()), 2 * (b.y() - a.y()), 2 * (b.z() - a.z()), 2 * (c.x() - a.x()), 2 * (c.y() - a.y()), 2 * (c.z() - a.z()), 2 * (d.x() - a.x()), 2 * (d.y() - a.y()), 2 * (d.z() - a.z()); auto sq = [](const double& val) { return val * val; }; Eigen::Vector3d RHS; RHS << (b.squaredNorm() - a.squaredNorm()), (c.squaredNorm() - a.squaredNorm()), (d.squaredNorm() - a.squaredNorm()); Eigen::Vector3d O = M.colPivHouseholderQr().solve(RHS); double r = (O - a).norm(); return Sphere(O, r); }
// Rには最大4頂点が入っている。 // それに応じて最小包含球を求める Sphere ball_from_R(const std::vector<Eigen::Vector3d>& R) { switch (R.size()) { case 0: return Sphere(Eigen::Vector3d::Zero(), 0.0); case 1: return ball_from_one_point(R[0]); case 2: return ball_from_two_points(R[0], R[1]); case 3: return ball_from_three_points(R[0], R[1], R[2]); case 4: return ball_from_four_points(R[0], R[1], R[2], R[3]); default: // それ以上はありえない return Sphere(); } }
//------------------------------------- // 点 p が球 S の中に入っているか判定 //-------------------------------------
inline bool is_in_sphere(const Eigen::Vector3d& p, const Sphere& S) { return (p - S.center).squaredNorm() <= S.radius * S.radius + 1e-14; // 浮動小数対策 }
//------------------------------------- // Welzlの再帰関数 //------------------------------------- // 引数: // points: 対象の点集合(インデックス0〜endで使用) // R: 球を定義するための点集合 // end: 再帰処理内での有効点数境界
Sphere welzl(const std::vector<Eigen::Vector3d>& points, std::vector<Eigen::Vector3d> R, int end) { // 再帰の末端、これ以上頂点がないか、Rが4点になったら球を求める if (end < 0 || R.size() == 4) { return ball_from_R(R); } // 頂点を一つ取り出して、それ以外の点で再帰処理 Eigen::Vector3d p = points[end]; Sphere D = welzl(points, R, end - 1); // 今取り出した点が、それ以外の点で求めた球に入っている場合 // 球を再計算する必要が無いので、そのまま返す if (is_in_sphere(p, D)) { return D; } // pがD内に収まらない場合 R.push_back(p); return welzl(points, R, end - 1); }
//------------------------------------- // 公開関数: 最小包含球を計算 //------------------------------------- Sphere minimum_enclosing_sphere(const std::vector<Eigen::Vector3d>& points) { // 頂点をシャッフルすると期待線形時間になるが // 頂点順を変えたくないため今回は使用しない // なお引数pointsがconst参照のため、ここを有効にしてもエラーになる。 // 有効化するときは引数のconstを外すか、コピーを作成すること。 #if 0 // ポイントシャッフル推奨(期待線形時間のため) std::random_device rd; std::mt19937 g(rd()); std::shuffle(points.begin(), points.end(), g); #endif std::vector<Eigen::Vector3d> dumyR; return welzl(points, dumyR, (int)points.size() - 1); }
}
std::pair<Eigen::Vector3f,float> CalcBoundingSphere(MyCloud& cloud) { welzl::Sphere S = welzl::minimum_enclosing_sphere(cloud.points); return { S.center.cast<float>(), S.radius }; }
// 点群をVTK用のActorに変換 vtkSmartPointer<vtkActor> ConvertCloudVTKActor(const MyCloud& cloud); // 座標と半径から球のActorを作成 vtkSmartPointer<vtkActor> CreateSphereActor(const std::pair<Eigen::Vector3f, float>& sphere_data); // ランダムな点群を作成 MyCloud CreateRandomCloud(int num_points); // 表示データを作成 bool PrepareData(vtkSmartPointer<vtkRenderer> renderer) { // ランダムな点群を作成 auto cloud = CreateRandomCloud(1000); // welzlでBounding Sphereを計算 auto sphere = CalcBoundingSphere(cloud); std::cout << "center : " << sphere.first.transpose() << std::endl; std::cout << "radius : " << sphere.second << std::endl; // VTK Actorを作成 auto cloudActor = ConvertCloudVTKActor(cloud); auto sphereActor = CreateSphereActor(sphere); // Create a vtkSphereSource vtkSmartPointer<vtkSphereSource> sphereSource = vtkSmartPointer<vtkSphereSource>::New(); sphereSource->SetCenter(sphere.first.x(), sphere.first.y(), sphere.first.z()); sphereSource->SetRadius(0.3); sphereSource->SetThetaResolution(32); sphereSource->SetPhiResolution(32); // Create a vtkPolyDataMapper vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New(); mapper->SetInputConnection(sphereSource->GetOutputPort()); // Create a vtkActor and assign the mapper vtkSmartPointer<vtkActor> CenterActor = vtkSmartPointer<vtkActor>::New(); CenterActor->SetMapper(mapper); renderer->AddActor(cloudActor); renderer->AddActor(sphereActor); renderer->AddActor(CenterActor); return true; } // 座標と半径から球のActorを作成 vtkSmartPointer<vtkActor> CreateSphereActor(const std::pair<Eigen::Vector3f, float>& sphere_data) { // 球の中心と半径を取得 const Eigen::Vector3f& center = sphere_data.first; float radius = sphere_data.second; // vtkSphereSourceを設定 vtkSmartPointer<vtkSphereSource> sphereSource = vtkSmartPointer<vtkSphereSource>::New(); sphereSource->SetCenter(center.x(), center.y(), center.z()); sphereSource->SetRadius(radius); sphereSource->SetThetaResolution(32); // 解像度設定 sphereSource->SetPhiResolution(32); // 解像度設定 // Mapperを作成 vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New(); mapper->SetInputConnection(sphereSource->GetOutputPort()); // Actorを作成 vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New(); actor->SetMapper(mapper); // 半透明に設定 actor->GetProperty()->SetOpacity(0.5); // 透明度(0.0~1.0) actor->GetProperty()->SetColor(1.0, 1.0, 1.0); // 白 return actor; }
// ランダムな点群を作成 MyCloud CreateRandomCloud(int num_points) { MyCloud cloud; std::random_device seed_gen; std::mt19937 engine(seed_gen()); std::uniform_real_distribution<> dist(-1.0, 1.0); for (int i = 0; i < num_points; i++) { cloud.points.emplace_back(dist(engine), dist(engine), dist(engine)); cloud.colors.emplace_back(dist(engine), dist(engine), dist(engine)); } return cloud; }
// 点群をVTK用のActorに変換 vtkSmartPointer<vtkActor> ConvertCloudVTKActor(const MyCloud& cloud) { // VTK用のポイントリストを作成 auto points = vtkSmartPointer<vtkPoints>::New(); for (const auto& point : cloud.points) { points->InsertNextPoint(point.x(), point.y(), point.z()); } // VTK用の頂点色リストを作成(Open3Dのcolors_を使用) auto colors = vtkSmartPointer<vtkUnsignedCharArray>::New(); colors->SetNumberOfComponents(3); // RGB if (!cloud.colors.empty()) { for (const auto& color : cloud.colors) { unsigned char r = static_cast<unsigned char>(color.x() * 255); unsigned char g = static_cast<unsigned char>(color.y() * 255); unsigned char b = static_cast<unsigned char>(color.z() * 255); colors->InsertNextTuple3(r, g, b); } } // ポリデータに点とオプションの属性(色、法線)を設定 auto polyData = vtkSmartPointer<vtkPolyData>::New(); polyData->SetPoints(points); if (!cloud.colors.empty()) { polyData->GetPointData()->SetScalars(colors); } // 点を頂点として描画可能にするフィルター auto glyphFilter = vtkSmartPointer<vtkVertexGlyphFilter>::New(); glyphFilter->SetInputData(polyData); glyphFilter->Update(); // マッパーを作成 auto mapper = vtkSmartPointer<vtkPolyDataMapper>::New(); mapper->SetInputData(glyphFilter->GetOutput()); if (!cloud.colors.empty()) { mapper->ScalarVisibilityOn(); // 頂点色を使用する } else { mapper->ScalarVisibilityOff(); // 頂点色がない場合はオフ } // Actorを作成 auto actor = vtkSmartPointer<vtkActor>::New(); actor->SetMapper(mapper); actor->GetProperty()->SetPointSize(5.0); actor->GetProperty()->SetInterpolationToFlat(); return actor; } int main() { vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New(); vtkSmartPointer<vtkRenderWindow> window = vtkSmartPointer<vtkRenderWindow>::New(); window->AddRenderer(renderer); vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New(); window->SetInteractor(interactor); PrepareData(renderer); window->SetSize(700, 400); interactor->Initialize(); window->Render(); interactor->Start(); }
WSLでインストールしたディストリビューションを直接変更できないので、一度.tarにエクスポートしてから名前を指定してインポートする。
一連の作業をpowershellでまとめた。
保存場所はインポートするディストリビューションの展開先なので、自分で専用の場所を決めておいたほうがいい。
$saveDirectory = Read-Host "保存場所 (デフォルト: ./distribution/)" # 入力が空の場合にデフォルト値を使用 if ([string]::IsNullOrWhiteSpace($saveDirectory)) { $saveDirectory = "./distribution/" } Write-Host "現在のディストリビューション一覧:" wsl -l -v # 名前を変更する $changeTarget = Read-Host -Prompt "変更するディストリビューション:" # 新たな名前を選択 $newname = Read-Host -Prompt "新たな名前" Write-Host "名前変更開始" Write-Host "現在のディストリビューションエクスポート" wsl --export $changeTarget ./temp_wsl.tar Write-Host "現在のディストリビューション削除" wsl --unregister $changeTarget Write-Host "インポート" wsl --import $newname $saveDirectory ./temp_wsl.tar Write-Host "一時ファイル削除" Remove-Item ./temp_wsl.tar #確認表示 wsl -l -v Write-Host "処理終了" Read-Host
skia-safeはSkiaのRustバインディング(非公式)。厳密にはskia-bindingというバインディングがあり、それをRustらしく使えるようにしたのがskia-safe。
use std::fs::File; use std::io::BufWriter;
fn main() { let width:u32 = 400; let height:u32 = 400; //(C++) SkBitmap bitmap; let mut bitmap = skia_safe::Bitmap::new(); //(C++) bitmap.setInfo(SkImageInfo::MakeN32Premul(400, 400)); let info = skia_safe::ImageInfo::new( (400,400), skia_safe::ColorType::N32, skia_safe::AlphaType::Premul, None ); bitmap.set_info(&info,None); //(C++) bitmap.allocPixels(); bitmap.alloc_pixels(); //(C++) SkCanvas canvas(bitmap); let mut canvas:skia_safe::OwnedCanvas = skia_safe::Canvas::from_bitmap(&bitmap,None).unwrap(); // bitmapの情報を表示 dispinfo(&bitmap); // 描画呼び出し draw(&mut canvas); //(C++) SkPngEncoder::Options options; //(C++) options.fZLibLevel = 9; // 圧縮レベル //(C++) options.fFilterFlags = SkPngEncoder::FilterFlag::kAll; // フィルタの種類 let mut opt = skia_safe::png_encoder::Options::default(); opt.z_lib_level = 9; opt.filter_flags = skia_safe::png_encoder::FilterFlag::ALL; opt.comments = vec![]; // ファイル出力 let file = File::create("out.png").unwrap(); let mut writer = BufWriter::new(file); let png_data = skia_safe::png_encoder::encode(&bitmap.pixmap(),&mut writer,&opt); }
fn draw(canvas:&mut skia_safe::OwnedCanvas){ //(C++) SkPaint paint; let mut paint: skia_safe::Paint = skia_safe::Paint::new(skia_safe::colors::BLACK,None); //(C++) paint.setStyle(SkPaint::kStroke_Style); paint.set_style(skia_safe::PaintStyle::Stroke); //(C++) paint.setStrokeWidth(5); paint.set_stroke_width(5.0); //(C++) paint.setColor(SK_ColorRED); paint.set_color(skia_safe::Color::RED); //(C++) canvas.drawCircle(200, 200, 100, paint); // (200,200) はタプル canvas.draw_circle((200,200),100.0,&paint); }
fn dispinfo(bitmap :&skia_safe::Bitmap) { let info = &bitmap.info(); let rowbytes = bitmap.row_bytes(); let channel = info.bytes_per_pixel(); let width = info.width(); let height = info.height(); println!("rowbytes:{}\nchannel:{}\nwidth:{}\nheight{}",rowbytes,channel,width,height); }
tiny-skiaはskiaの影響を受けたライブラリで、テキストレンダリングなどの機能を除いて「skiaっぽくつかえる軽量のライブラリ」として開発されている。本家とは関係ない。BSDライセンス。
[dependencies]
# tiny-skiaの使用。
# https://crates.io/crates/tiny-skia/
tiny-skia = "0.11"
# 乱数を使用 https://crates.io/crates/rand
rand = "0.8.5"
※コード中の「skia」は全てtiny-skiaの意味。
use rand::Rng;// 乱数用 use tiny_skia::{Pixmap, Paint, Color, PathBuilder, Transform};
fn main() { let width:u32 = 400; let height:u32 = 400; // skiaの画像を作成 let mut skiaimage = Pixmap::new(width,height).expect("skia error"); // イテレータ経由でピクセルバッファへアクセス // 全ての画素を緑で塗りつぶし let mut pixel_itr = skiaimage.data_mut().chunks_exact_mut(4); for pixel in pixel_itr{ pixel[0] = 0; pixel[1] = 255; // 緑 pixel[2] = 0; pixel[3] = 255; } // tiny-skiaで円を描く circle(&mut skiaimage,200.0,200.0,100.0,0,0,255); circle(&mut skiaimage,130.0,250.0,80.0,255,0,0); let mut rng = rand::thread_rng(); // 乱数を使用 // ランダムアクセス // スライスの配列を作成してアクセスするので、使い回さないのなら効率が悪い let mut pixels: Vec<&mut [u8]> = skiaimage.data_mut().chunks_exact_mut(4).collect(); (0..50000).for_each(|_|{ let x:u32=rng.gen_range(0 ..width as i32) as u32; let y:u32=rng.gen_range(0 .. height as i32) as u32; let pos = (y*width+x) as usize; pixels[pos][0] =255; pixels[pos][1] =255; pixels[pos][2] =255; pixels[pos][3] =255; }); // 結果をPNGに保存 skiaimage.save_png("output.png").expect("Failed to save PNG"); println!("Image saved as output.png"); }
fn circle(simg:&mut Pixmap,px:f32,py:f32,radius:f32,r:u8,g:u8,b:u8){ // 塗りつぶしの色を設定 let mut paint = Paint::default(); paint.set_color(Color::from_rgba8(r,g,b, 255)); // パス作成 let mut path_builder = PathBuilder::new(); path_builder.push_circle(px,py,radius); let path = path_builder.finish().expect("Failed to create path"); // 円を描画 simg.fill_path( &path, &paint, tiny_skia::FillRule::Winding, Transform::identity(), None, ); }
配列をそのままフロントエンド(HTML+Javascript)に転送する。
// 返却用の構造体のために必要 use serde::Serialize; // 値を返す構造体 #[derive(Serialize)] struct ImageData{ image:Vec<u8>, width:u32, height:u32, } // 画像と情報を返す関数 #[tauri::command] fn get_image() -> ImageData { let width:u32 = 256; let height:u32 = 400; // 画像生成 let mut imgrgb = Vec::<u8>::new(); for x in 0..width{ let r = (x % height) as u8; for _ in 0..height{ imgrgb.push(((r as usize ) % 256) as u8); imgrgb.push(0); imgrgb.push(0); } } // 返却 ImageData { image: imgrgb, width: width, height: height, } } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![get_image]) // get_image関数を登録 .run(tauri::generate_context!()) .expect("error while running Tauri application"); }
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Rust+Tauri画像表示</title> <style> canvas#mycanvas { border: 1px solid #000; } </style> <script type="module" src="/main.js" defer></script> </head> <body> <h1>Rust+Tauri画像表示</h1> <button id="ButtonLoadImage">画像表示</button> <canvas id="mycanvas" width="640" height="480"></canvas> </body> </html>
const { invoke } = window.__TAURI__.core; document.getElementById('ButtonLoadImage').addEventListener('click',async()=>{ const mycanvas = document.getElementById('mycanvas'); const ctx = mycanvas.getContext('2d'); const ImageData = await window.__TAURI__.core.invoke("get_image"); const rustArray = new Uint8Array(ImageData.image); // RGB8の配列 const width = ImageData.width; const height = ImageData.height; // Canvasサイズを調整 mycanvas.width = width; mycanvas.height = height; // キャンバスの画像データを作成 const CanvasImageData = ctx.createImageData(width,height); // キャンバスに与えるデータへの参照を取得 const tmp = CanvasImageData.data; // Rust側がRGBを返すので、RGBA形式で格納 for(let i=0;i<width*height;i++){ tmp[i*4+0] = rustArray[i*3+0]; tmp[i*4+1] = rustArray[i*3+1]; tmp[i*4+2] = rustArray[i*3+2]; tmp[i*4+3] = 255; } ctx.putImageData(CanvasImageData,0,0); });
上ではデータをrgbで送ったため、Canvasが対応するrgbaに変換するためにforを回す必要があったが、rgbaであれば直接コピーできる。
const { invoke } = window.__TAURI__.core; document.getElementById('ButtonLoadImage').addEventListener('click',async()=>{ const mycanvas = document.getElementById('mycanvas'); const ctx = mycanvas.getContext('2d'); const ImageData = await window.__TAURI__.core.invoke("get_image"/*,{index}*/ ); const rustArray = new Uint8Array(ImageData.image); // RGB8の配列 const width = ImageData.width; const height = ImageData.height; // Canvasサイズを調整 mycanvas.width = width; mycanvas.height = height; // キャンバスの画像データを作成 const CanvasImageData = ctx.createImageData(width,height); // キャンバスにRustから受け取ったデータをセット CanvasImageData.data.set(rustArray); ctx.putImageData(CanvasImageData,0,0); });
画像を高速に表示しようとするとWebGLを使うのがいいらしいが、いろいろと手間なので、まずはRust側でbase64に変換してimgに設定する方法を試す。高いフレームレートが要求される用途でなければ、十分に機能する。
use base64::{engine::general_purpose, Engine as _}; use std::io::Cursor; use image::codecs::png::PngEncoder; use image::ImageEncoder; // 返却用の構造体のために必要 use serde::Serialize; // 値を返す構造体 #[derive(Serialize)] struct ImageData{ image:String, width:u32, height:u32, } // 画像と情報を返す関数 #[tauri::command] fn get_image(index: usize) -> ImageData { let width:u32 = 256; let height:u32 = 400; // 画像生成 let mut imgrgb = Vec::<u8>::new(); for x in 0..width{ let r = (x % height) as u8; for y in 0..height{ imgrgb.push(((r as usize * index) % 256) as u8); imgrgb.push(0); imgrgb.push(0); } } // PNGデータにエンコード // RGB配列 → PNG形式への変換 let mut buffer = Cursor::new(Vec::new()); let encoder = PngEncoder::new(&mut buffer); encoder.write_image( &imgrgb, width, height, image::ColorType::Rgb8 ).unwrap(); // Base64エンコードして返す // PNG形式 → Base64表現への変換 let base64_image = general_purpose::STANDARD.encode(buffer.get_ref()); // フロントエンドでimgタグに設定できる形式にする let img =format!("data:image/png;base64,{}", base64_image); // 返却 ImageData { image: img, width: width, height: height, } } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![get_image]) // get_image関数をフロントエンドが呼び出せるようにする .run(tauri::generate_context!()) .expect("error while running Tauri application"); }
[package]
name = "my1st_tauri"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
image = "0.24"
base64 = "0.21"
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Rust+Tauri画像表示</title> </head> <body> <h1>Rust+Tauri画像表示</h1> <img id="image-viewer" alt="Image Viewer" style="border: 1px solid black; width: 256px; height: 256px;"> <p>左右キーで再読み込み</p> <script> let currentIndex = 0;
async function updateImage(index) { // Rustから画像データを取得 const imageData = await window.__TAURI__.core.invoke("get_image", { index }); imageview = document.getElementById("image-viewer"); // 表示領域の取得 imageview.src = imageData.image; // 画像を設定 imageview.style.width = `${imageData.width}px`; // 画像の幅を設定 imageview.style.height = `${imageData.height}px`; // 画像の高さを設定 }
// 初期画像を読み込み updateImage(currentIndex);
// キーイベントリスナー document.addEventListener("keydown", (event) => { if (event.key === "ArrowRight") { currentIndex++; updateImage(currentIndex); } else if (event.key === "ArrowLeft") { currentIndex = Math.max(0, currentIndex - 1); updateImage(currentIndex); } });
</script> </body> </html>
久しぶりにTauriを学ぼうと思い、以前の記事を参考にプロジェクトを作ってみた
すると以下のエラーが発生
これはcreate-tauri-appが作成するCargo.tomlが現在インストールされているTauriライブラリのバージョンに対応していないことが由来らしい。
ここで、create-tauri-appのバージョンを調べてみると
npm list -g create-tauri-app
C:\application\node.js\node-v21.6.2-win-x64
`-- (empty)
となっていて、create-tauri-appのバージョンがemptyになっている。このとき、create-tauri-appは必要な時だけ最新版がダウンロードされて使用されるらしい。つまりcreate-tauri-appは最新版が使われるということ。ここで、cargo install tauri-cliを実行してみると
error: failed to compile `tauri-cli v2.1.0`, intermediate artifacts can be found at `C:\User\myuser\AppData\Local\Temp\cargo-installSnryuD`.
To reuse those artifacts with a future compilation, set the environment variable `CARGO_TARGET_DIR` to that path.
Caused by:
package `tauri-cli v2.1.0` cannot be built because it requires rustc 1.77.2 or newer, while the currently active rustc version is 1.74.1
Try re-running cargo install with `--locked`
と、「現在のrustcのバージョンが1.74.1だが、最新のtauri-cliのバージョンはrustc 1.77.2以上を要求する」という理由でtauri-cliのインストールが失敗する。
つまり、プロジェクトを作るcreate-tauri-appのバージョンが常に最新版が使われているが、tauriライブラリの管理などを行うtauri-cliがructc 1.74対応版という古いものなので、最新のプロジェクトをビルドできないということだと思う。
create-tauri-appのバージョンを1.74版に指定すればうまくいきそうだが、低いバージョンを使う意味も今のところないので、rustcのバージョンアップをして対応する。
なおバージョンチェックは
rustc --version
で行う。
これでnpx create-tauri-appでプロジェクトを作成できるようになった。
なお、最新版ではIdentifierを作成時に指定できるので、手動でbundle > identifierを変更する必要もない。
glTranslated等のレガシーな関数が非推奨になっているので移動行列や回転行列や射影行列を自分で計算しないといけない。面倒なのでクレートを使う。いくつかあるが、cgmathがglmに近い使い勝手らしい。
以下のようにCargo.tomlにcgmathを追加する。
[package]
name = "opengltest"
version = "0.1.0"
edition = "2021"
[dependencies]
glutin = "0.26.0"
gl="0.14.0"
cgmath = "0.18.0"
以下、translate,rotate,perspective view matrixの使用例。
pub fn draw_triangle(programid:gl::types::GLuint,vertexbuffer: gl::types::GLuint,colorbuffer:gl::types::GLuint){ unsafe { gl::UseProgram(programid); } let translate_mat = cgmath::Matrix4::from_translation( cgmath::Vector3::new(0.0,0.0,-3.0) ); let rotate_mat = cgmath::Matrix4::from_angle_z(cgmath::Deg(45.0)); let model_mat = rotate_mat * translate_mat; let fov = cgmath::Deg(45.0); let aspect = 1.0; let near = 0.1; let far = 100.0; let proj_mat = cgmath::perspective(fov,aspect,near,far); let proj; let model; unsafe { let proj_location_name = CString::new("projectionMatrix").unwrap(); let model_location_name = CString::new("modelViewMatrix").unwrap(); proj = gl::GetUniformLocation(programid,proj_location_name.as_ptr()); model = gl::GetUniformLocation(programid,model_location_name.as_ptr()); gl::UniformMatrix4fv(proj,1,gl::FALSE,proj_mat.as_ptr()); gl::UniformMatrix4fv(model,1,gl::FALSE,model_mat.as_ptr()); } /* // デバッグ println!("proj location: {}",proj); println!("model location: {}",model); */ unsafe{ gl::EnableVertexAttribArray(0); gl::BindBuffer(gl::ARRAY_BUFFER,vertexbuffer); gl::VertexAttribPointer( 0, 3, gl::FLOAT, gl::FALSE, 0, null() ); } unsafe{ gl::EnableVertexAttribArray(1); gl::BindBuffer(gl::ARRAY_BUFFER,colorbuffer); gl::VertexAttribPointer( 1, 3, gl::FLOAT, gl::FALSE, 0, null() ); } unsafe { gl::DrawArrays(gl::TRIANGLES, 0,3); let err_check = gl::GetError(); if err_check != gl::NO_ERROR { println!("ERROR::: {}\n", err_check); } gl::DisableVertexAttribArray(0); gl::DisableVertexAttribArray(1); } unsafe { gl::UseProgram(0); } }
以前初期化はやったことがあるが、今回はちゃんと三角形を描く。
基本的にglBeginのような古い関数は使えないので、GLSLで書くことになる。
Rust内の文字列をC言語の文字列に変換するところがいちいちややこしい。
use std::ptr::null; use std::ffi::CString; use std::ffi::CStr;
pub fn draw_prepare() ->(gl::types::GLuint, gl::types::GLuint){ // 頂点の定義 let vertices: [f32;9]=[ 0.0, 0.5, 0.0, // 上頂点 -0.5, -0.5, 0.0, // 左下 0.5, -0.5, 0.0, // 右下 ]; let color:[f32;9]=[ 1.0,0.0,0.0, 0.0,1.0,0.0, 0.0,0.0,1.0, ]; let mut vertexbuffer=0; unsafe{ gl::GenBuffers(1,&mut vertexbuffer); gl::BindBuffer(gl::ARRAY_BUFFER,vertexbuffer); gl::BufferData( gl::ARRAY_BUFFER, 3*3*std::mem::size_of::<gl::types::GLfloat>() as gl::types::GLsizeiptr, vertices.as_ptr() as *const _, gl::STATIC_DRAW ); } let mut colorbuffer=0; unsafe{ gl::GenBuffers(1,&mut colorbuffer); gl::BindBuffer(gl::ARRAY_BUFFER,colorbuffer); gl::BufferData( gl::ARRAY_BUFFER, 3*3*std::mem::size_of::<gl::types::GLfloat>() as gl::types::GLsizeiptr, color.as_ptr() as *const _, gl::STATIC_DRAW ); } (vertexbuffer,colorbuffer) }
pub fn prepare_vertex_shader()->gl::types::GLuint{ let mut VertexShaderID = 0; // 頂点シェーダプログラム let vertex_shader_source = "\ #version 460 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 incolor; out vec4 vertexColor; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(aPos, 1.0); vertexColor = vec4(incolor, 1.0); } "; let c_src = CString::new(vertex_shader_source).unwrap(); let mut Result:gl::types::GLint = 0; let mut InfoLogLength:i32 = 0; let mut info_log; unsafe{ VertexShaderID = gl::CreateShader(gl::VERTEX_SHADER); gl::ShaderSource( VertexShaderID, 1, &c_src.as_ptr(), std::ptr::null() ); gl::CompileShader(VertexShaderID); // シェーダのチェック gl::GetShaderiv(VertexShaderID,gl::COMPILE_STATUS,&mut Result); gl::GetShaderiv(VertexShaderID,gl::INFO_LOG_LENGTH,&mut InfoLogLength); if InfoLogLength > 0 { info_log = vec![0u8; InfoLogLength as usize]; if Result == gl::FALSE as i32 { gl::GetShaderInfoLog( VertexShaderID, InfoLogLength, std::ptr::null_mut(), info_log.as_mut_ptr() as *mut gl::types::GLchar ); if let Ok(msg) = CStr::from_ptr(info_log.as_ptr() as *const i8).to_str() { println!("Vertex Shader Error :\n {}\n", msg); } } } } VertexShaderID }
pub fn prepare_fragment_shader()->gl::types::GLuint{ let mut FragmentShaderID=0; let fragment_shader_source = "\ #version 460 core out vec4 FragColor; in vec4 vertexColor; void main() { FragColor = vertexColor; } "; let c_str = CString::new(fragment_shader_source).unwrap(); let mut Result:gl::types::GLint=0; let mut InfoLogLength:i32=0; let mut info_log; unsafe { FragmentShaderID = gl::CreateShader(gl::FRAGMENT_SHADER); gl::ShaderSource( FragmentShaderID, 1, &c_str.as_ptr(), null() ); gl::CompileShader(FragmentShaderID); // フラグメントシェーダ gl::GetShaderiv(FragmentShaderID, gl::COMPILE_STATUS, &mut Result); gl::GetShaderiv(FragmentShaderID, gl::INFO_LOG_LENGTH, &mut InfoLogLength); if InfoLogLength > 0 { if Result == gl::FALSE as i32 { info_log = vec![0u8; InfoLogLength as usize]; gl::GetShaderInfoLog( FragmentShaderID, InfoLogLength, std::ptr::null_mut(), info_log.as_mut_ptr() as *mut gl::types::GLchar ); if let Ok(msg) = CStr::from_ptr(info_log.as_ptr() as *const i8).to_str() { println!("Fragment Shader Error :\n{}\n", msg); } } } } FragmentShaderID }
pub fn link_program(VertexShaderID:gl::types::GLuint,FragmentShaderID:gl::types::GLuint)->gl::types::GLuint{ let mut Result:gl::types::GLint = gl::FALSE as i32; let mut InfoLogLength:i32=0; let mut ProgramID:gl::types::GLuint=0; println!("Linking program"); unsafe{ ProgramID = gl::CreateProgram(); gl::AttachShader(ProgramID,VertexShaderID); gl::AttachShader(ProgramID,FragmentShaderID); gl::LinkProgram(ProgramID); gl::GetProgramiv(ProgramID,gl::LINK_STATUS,&mut Result); gl::GetProgramiv(ProgramID,gl::INFO_LOG_LENGTH,&mut InfoLogLength); if InfoLogLength > 0 { let mut ProgramErrorMessage = vec![0u8; InfoLogLength as usize]; gl::GetProgramInfoLog( ProgramID, InfoLogLength, std::ptr::null_mut(), ProgramErrorMessage.as_mut_ptr() as *mut gl::types::GLchar ); if let Ok(msg) = CStr::from_ptr(ProgramErrorMessage.as_ptr() as *const i8).to_str() { println!("Program Link Error:\n{}\n",msg); } } } ProgramID }
pub fn draw_triangle(programid:gl::types::GLuint,vertexbuffer: gl::types::GLuint,colorbuffer:gl::types::GLuint){ unsafe { gl::UseProgram(programid); } let proj_mat:[f32;16]=[ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ]; let model_mat:[f32;16]=[ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, ]; let proj; let model; unsafe { let proj_location_name = CString::new("projectionMatrix").unwrap(); let model_location_name = CString::new("modelViewMatrix").unwrap(); proj = gl::GetUniformLocation(programid,proj_location_name.as_ptr()); model = gl::GetUniformLocation(programid,model_location_name.as_ptr()); gl::UniformMatrix4fv(proj,1,gl::FALSE,proj_mat.as_ptr()); gl::UniformMatrix4fv(model,1,gl::FALSE,model_mat.as_ptr()); } /* // デバッグ println!("proj location: {}",proj); println!("model location: {}",model); */ unsafe{ gl::EnableVertexAttribArray(0); gl::BindBuffer(gl::ARRAY_BUFFER,vertexbuffer); gl::VertexAttribPointer( 0, 3, gl::FLOAT, gl::FALSE, 0, null() ); } unsafe{ gl::EnableVertexAttribArray(1); gl::BindBuffer(gl::ARRAY_BUFFER,colorbuffer); gl::VertexAttribPointer( 1, 3, gl::FLOAT, gl::FALSE, 0, null() ); } unsafe { gl::DrawArrays(gl::TRIANGLES, 0,3); let err_check = gl::GetError(); if err_check != gl::NO_ERROR { println!("ERROR::: {}\n", err_check); } gl::DisableVertexAttribArray(0); gl::DisableVertexAttribArray(1); } unsafe { gl::UseProgram(0); } }
// 自作ファイル draw.rs の使用 mod draw; // 自作関数を呼び出せるようにする use draw::draw_triangle; use draw::draw_prepare; use draw::prepare_vertex_shader; use draw::prepare_fragment_shader; use draw::link_program; fn main() { let event_loop = glutin::event_loop::EventLoop::new(); let window = glutin::window::WindowBuilder::new().with_title("Rust glutin OpenGL"); // GLのコンテキストを作成 // この gl_context は ContextBuilder<NotCurrent,Window> 型 let gl_context = glutin::ContextBuilder::new() // コアプロファイルを (3, 1) で3.1 に指定。3.2以上だとVAOが必須になりコードの追記が必要なため .with_gl(glutin::GlRequest::Specific(glutin::Api::OpenGl, (3, 1))) .build_windowed(window, &event_loop) .expect("Cannot create context"); // Rustにはシャドーイングがあるので、同じ名前の別変数を同じスコープ内に定義できる。 // この gl_context は ContextBuilder<PossibleCurrent,Window> 型 // 以降、gl_currentは以前の型の意味で用いることはできない let gl_context = unsafe { gl_context .make_current() .expect("Failed to make context current") }; // OpenGLの各関数を初期化 // これでgl::Viewportをはじめとする各関数の関数ポインタにアドレスが設定され、呼び出せるようになる。 gl::load_with(|symbol| gl_context.get_proc_address(symbol) as *const _); /////////////////////////////////////////////////////////////////////////////////// let (vbuffer,cbuffer) = draw_prepare(); // 座標と色のバッファを定義 let VertexShaderID:gl::types::GLuint = prepare_vertex_shader(); // バーテクスシェーダ作成 let FragmentShaderID:gl::types::GLuint = prepare_fragment_shader(); // フラグメントシェーダ作成 let ProgramID:gl::types::GLuint = link_program(VertexShaderID,FragmentShaderID); // プログラムのリンク /////////////////////////////////////////////////////////////////////////////////// // event_loopを開始する event_loop.run(move |event, _, control_flow| { // Pollを指定するとループが走り続ける // Waitを指定するとイベントが発生したときだけループが動く。 *control_flow = glutin::event_loop::ControlFlow::Wait; match event { glutin::event::Event::WindowEvent { event, .. } => match event { //////////////////////////////////////////////////////////////////// // ウィンドウを閉じる glutin::event::WindowEvent::CloseRequested => { // ウィンドウが閉じられた場合、event_loopを停止する *control_flow = glutin::event_loop::ControlFlow::Exit; }, //////////////////////////////////////////////////////////////////// // ウィンドウのサイズを変更 glutin::event::WindowEvent::Resized(new_size) => { // ビューポート再設定 unsafe { gl::Viewport(0, 0, new_size.width as i32, new_size.height as i32); } }, //////////////////////////////////////////////////////////////////// _ => (), }, _ => (), } // 描画 unsafe { gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT); gl::ClearColor(0.3, 0.3, 0.5, 1.0); gl::Disable(gl::DEPTH_TEST); gl::Disable(gl::CULL_FACE); // 描画 draw_triangle(ProgramID,vbuffer,cbuffer); gl::Flush(); } // スワップバッファ gl_context.swap_buffers().unwrap(); }); }
Rust用の統合開発環境RustRoverが、非商用に限り無料で使用できるらしい。
インストール後、起動するとライセンスの種類を選択できるので、無料ライセンスを選ぶ。
新規プロジェクトを作る。
実行。特に何もしなくても、▷ボタンで実行できる。
デバッグ用のコンソールはUnicodeに対応しているらしく絵文字もそのまま表示できる。
昔RustでWin32APIを使ってみたが、それも何の工夫もせずにちゃんと実行できた。
個人的に極めて重要なのが、Ctrl+ホイールでエディタの文字サイズ変更がどうしても欲しい。あとテーマはLightがいい。
エディター→一般→インライン補完→ローカルのFull Line補完候補を有効にするのチェックを外す。
これが入っていると調べなくても書けてしまうので上達しない。