この記事は Siv3D Advent Calendar 2025 19日目です。
Happy Holiday! こんにちは。natsukodachiです。遅刻してごめんなさい。
はじめに
今回はnetCDF形式の気象データをOpenSiv3Dで描写していこうと思います。
気象データの可視化は、ほとんどの場合Gradsかpythonが用いられます。しかし、Fortranなどのコンパイル言語に慣れている気象関係者にとって、syntaxがきかないGradsスクリプトや全く記法が異なるpythonは中々とっつきにくいのではないでしょうか。
そこで、速い・書きやすい・かっこいいの三拍子が揃ったC++で作図します。C++さいこー!
環境・使用ツール
- OpenSiv3D v0.6
- netcdf-cxx4:x64-windows-static (静的CRT版)
- vcpkgからインストールする。詳しくは別記事を参照ください
- Visual Studio 2026
- (ncdump)
- データの確認用
Debugビルド時、Siv3Dは/MTd(静的ビルド)、通常のnetcdfは/MDd(動的ビルド)でビルドされ、これらは両立しない。そのため、netcdf側で静的ビルド版を取ってくる必要がある。
また、VcpkgTripletを明示する必要がある。
- プロジェクトのプロパティ>vcpkg>Use Static Libraries>はい
- プロジェクトのプロパティ>vcpkg>Triplet→x64-windows-static
環境によっては、次のビルドエラーが発生することがあります。
libucrt.lib(stat.obj) : error LNK2005: _fstat64 は既に netcdf.lib(zmap_file.c.obj) で定義されています。
この場合、応急処置として、プロジェクトのプロパティ>リンカー>全般>ファイルを強制的に出力>/Force:Multipleを選択するとビルドが通るようになります。
データ取得・読みこみ
欧州中期予報センター(ECMWF)の再解析データERA5を使用する。Climate Data Store (CDS)からいい感じのデータをとってくる。今回は台風日の気圧を見たいので、台風15号が接近した2025/9/5 UTC 3:00のPMSLを選んだ。
データ読み込みはnetcdf-cxxで行うので(アドカレの主旨とずれるので)詳しい説明は省略する。
緯度、経度、地点毎のデータを格納するArray、Gridを作成し、代入するようにする。
#include <Siv3D.hpp>
#include <netcdf>
using namespace netCDF;
struct weatherData
{
Grid<double> msl;
Array<double> lats;
Array<double> lons;
};
void LoadZ500(weatherData& field, const FilePath& ncPath)
{
// NetCDF ファイルを開く
NcFile nc(Unicode::Narrow(ncPath), NcFile::read);
//緯度、経度、pmslのデータと要素数を取得する
//配列をresizeする
//代入する
}
カラーマップを作成する
Colormap01()でカラーマップを作成することができる。第二引数で使用するカラーマップの種類が選べる。
Image CreateColormapImage(const Grid<double>& msl, double vmin, double vmax, ColormapType cmapType)
{
// 値の範囲を 0.0-1.0 に正規化
const double invRange = 1.0 / (vmax - vmin);
const int32 w = static_cast<int32>(msl.width());
const int32 h = static_cast<int32>(msl.height());
Image img(w, h);
for (int32 y = 0; y < h; ++y)
{
for (int32 x = 0; x < w; ++x)
{
double t = (msl[y][x] - vmin) * invRange;
t = Clamp(t, 0.0, 1.0);
img[y][x] = Colormap01F(t, cmapType);
}
}
return img;
}
海岸線を描写する
公式サイトにgeojsonを使った世界地図のサンプルがあるので、これを参考にして日本域を描く。
class CoastlineOverlay
{
public:
// 可視範囲を受け取り、範囲に入る国だけ抽出
CoastlineOverlay(const RectF& geoViewRectLonY)
{
countries_ = GeoJSONFeatureCollection{ JSON::Load(U"example/geojson/countries.geojson") }
.getFeatures()
.map([](const GeoJSONFeature& f) { return f.getGeometry().getPolygons(); });
// 可視範囲内の国だけ抽出
visibleIndices_.reserve(countries_.size());
for (auto&& [i, country] : Indexed(countries_))
{
if (country.computeBoundingRect().intersects(geoViewRectLonY))
{
visibleIndices_ << i;
}
}
}
// テクスチャの描画領域(destRect)に合わせて海岸線を重ね描き
void draw(const RectF& destRect, double lonMin, double lonMax, double yMin, double yMax) const
{
// グリッドの 1 ピクセルサイズ
const Vec2 pixelSize = destRect.size / Vec2{ 1.0 * m_w, 1.0 * m_h };
const Vec2 halfPixel = pixelSize * 0.5;
// 経度・緯度(y は-latitude)→ピクセル座標へのスケール
const double sx = (destRect.w - pixelSize.x) / (lonMax - lonMin); // (w-1) 相当
const double sy = (destRect.h - pixelSize.y) / (yMax - yMin); // (h-1) 相当
// 左上原点合わせ(半ピクセル補正込み)
const Vec2 translate = (destRect.pos + halfPixel) - Vec2{ lonMin * sx, yMin * sy };
// 座標変換の適用
const Transformer2D t{ Mat3x2::Scale(Vec2{ sx, sy }).translated(translate) };
// 画面拡大率に応じた線幅(スケール非依存)
const double lineThickness = (1.0 / Graphics2D::GetMaxScaling());
// 可視国のみ描画
for (const size_t i : visibleIndices_)
{
countries_[i].drawFrame(lineThickness, ColorF{ 0.1, 0.1, 0.1, 0.85 });
}
}
// グリッドサイズ(ピクセル数)を設定
void setGridSize(int32 w, int32 h)
{
m_w = w;
m_h = h;
}
private:
Array<MultiPolygon> countries_; // 国境線ポリゴン
Array<size_t> visibleIndices_; // 可視範囲に入る国のインデックス
int32 m_w = 1; // グリッド幅
int32 m_h = 1; // グリッド高さ
};
結果
コードはこちらです。
近畿中部の気圧が低いことが分かる。
おわり
思ってたより大変だったが、なんとか描けてよかった。
風の矢羽根を描いたり、圧力の等高線を描いたりとかもやりたい。
ちなみに、netCDFはバイナリなので頑張ればSiv3Dオンリーで完結することができる。が、それはまたの機会に。

