バーテックスシェーダー・フラグメントシェーダーで構成されたレンダリングパイプライン < 点を描くところから始めるRust製ソフトウェアラスタライザ
この記事の前提
点を描くところから始めるRust製ソフトウェアラスタライザ
from テクスチャマッピング < 点を描くところから始めるRust製ソフトウェアラスタライザ
https://gyazo.com/93940f985604721bab067510b16325f4
バーテックスシェーダー・フラグメントシェーダーで構成されたレンダリングパイプライン < 点を描くところから始めるRust製ソフトウェアラスタライザ
テクスチャマッピング < 点を描くところから始めるRust製ソフトウェアラスタライザでは、ちょっと無理やり組みすぎたので、ちゃんとコードの形式を整えていく。
やったことは
TexturedMeshの定義(頂点データが座標値とuv値を持つようなメッシュデータ)
板ポリのメッシュ
TexturedMeshに対応したMeshRenderer
texture_fshaderの実装
ワールドの実装
この3つだけど、「TexturedMeshに対応したMeshRenderer」でMeshRendererのコードをコピーしてTexturedMeshを作った。これをどうにかしたい。
これはDRY原則に反しているので、できれば避けたい
ほとんどのコードは共通しているので、それに対処したい。
違うのはMeshの渡し方の部分と、RenderingPipelineだけ!
できれば、MeshRender一つで済ませられるといいな
RenderingPipelineへの分離を考えるappbird.icon
基本的に、3DCGでやるような様式に整えていきたい
ライティングの方法が変わるたびにいちいちMeshRendererが生まれるのはたまったもんではない...。
code:shader/pipeline.rs
pub trait RenderingPipeline<
Uniform,
Attribute,
Varying,
where Varying:for<'a> Add<&'a Varying> + Mul<f64> {
fn vertex(uni:&Uniform, vert:&Attribute) -> Varying;
fn fragment(uni:&Uniform, vary:&Varying) -> Vec4;
}
Varyingはスカラー倍, 加算を実装しておく必要がある ---> 補間のためappbird.icon
したがって、Textureのサンプリングをするシェーダーを実装していく
code:rs
struct Varying {
depth:Vec4,
uv:Vec4
}
impl Interpolable for Varying {
fn interpolate(p: &Self; 3, w: f64; 3) -> Self {
Self {
depth: &p0.depth * w0 + &p1.depth * w1 + &p2.depth * w2,
uv: &p0.uv * w0 + &p1.uv * w1 + &p2.uv * w2,
}
}
}
う、うーん....打ち間違えやすそうで怖いな
というわけでmacroの出番。
deriveマクロを使いたいのでやってみる。
code:rs
#derive(Interpolate)
struct Varying {
uv:Vec4
}
これやったらinterpolateの実装が生えてくるようにしたい。まぁまずGPTに聞いてみよう
別プロジェクトを生やす必要があるらしい、まじかあappbird.icon
workspaceを作って、coreプロジェクトとinterpolate_deriveプロジェクトを作る
そのためにはderiveの仕様を探る必要がある....
accessed at 2025-10-13 18:26
You can implement derive for your own traits through procedural macros.
なるほど
字句解析されたTokenStreamからTokenを一個一個受け取って、別のTokenStreamを生成するようなコードを描けばいいらしい。
まずTokenStreamから引っ張ってきて、構文解析しておく。
構文解析はparse_macro_input!マクロでなんとかなる
inputからは構造体の名前が取れるので引っ張っておく。
code:interpolate_derive/src/lib.rs
#proc_macro_derive(Interpolate)
pub fn derive_interpolable(input:TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
...
}
そのうえで、構造体のフィールドのデータを引き出しておく。
Fields::Namedの部分に格納されているので、Vecに識別子名を入れておく。
流し込まれたブロックがstruct以外だったらエラーを吐いておく。
code:rs
let fields = match input.data {
Data::Struct(ref s) => match &s.fields {
Fields::Named(f) => f.named.iter().map(|f| f.ident.clone().unwrap()).collect::<Vec<_>>(),
_ => panic!("Interpolable can only be derived for structs with named fields"),
},
_ => panic!("Interpolable can only be derived for structs"),
};
そして、最後に生成したいトークン列としてTokenStreamを生成する。
#(...)*でトークン列fields:Vec<Ident>を展開できる
code:rs
let expanded = quote! {
impl Interpolable for #name {
fn interpolate(p:&Self;3, w:f64; 3) -> Self {
Self {
#(
#fields: &p0.#fields * w0 + &p1.#fields * w1 + &p2.#fields * w2,
)*
}
}
}
};
上記で生成されたトークン列はproc_macro2のものなので、proc_macroのものに変換しておく。
TokenStream::from(expanded)
proc_macro2はproc_macroの機能群を手続き型マクロ以外の箇所でも使えるようにするライブラリみたい
単体テストもできるようにいろいろ機能が加えられているらしい。
これでimplブロックを自動生成できるようになった。
code:shader/texture_pipeline.rs
#derive(Interpolate)
struct Varying {
uv:Vec4
}
と書けば、それぞれのフィールドに対して補間操作を行える機能を追加するimplブロックが自動生成される。
続けてRenderingPipelineをつくる。
TextureSamplingの実装を写してくる
少しuvの実装がトリッキーになる。
まずUniform, Attribute, Varyingを実装しておく
code:shader/texture_pipeline.rs
pub struct TexturePipeline;
struct Uniform {
pvm:Mat4x4,
size:Point2,
texture:Rgba32FImage
}
struct Attribute {
point:Vec4,
uv:Vec4
}
#derive(Interpolate)
struct Varying {
uv:Vec4
}
続けて、fragmentは前の実装をほぼそのまま持ってくればOK
code:rs
impl RenderingPipeline<Uniform, Attribute, Varying> for TexturePipeline{
fn fragment(_p:&Point2, uni:&Uniform, vary:&Varying) -> Vec4 {
let (w, h) = uni.texture.dimensions();
let (u, v) = (vary.uv.x() * w as f64, vary.uv.y() * h as f64);
let (u, v) = (u as u32, v as u32);
let pixel = uni.texture.get_pixel_checked(u, v);
if let Some(c) = pixel {
Vec4::new(c0 as f64, c1 as f64, c2 as f64, c3 as f64)
} else {
Vec4::new(0., 0., 0., 0.)
}
}
}
vertexの実装は少しトリッキーになる。
uv値のように奥行値を意識しなくてはいけない場合、$ u = \sum_i (w_i(z/z_i)) u_i となるが、係数は$ w_1, w_2, w_3で補間される。
そのため、補間側に渡す係数を調節する必要がある。
$ u/z = \sum_i w_i(u_i/z_i)
$ zは描画点に関係するものなので、$ zはFragment側じゃないと取り出せない
こここう置き換えないとダメか
code:rs
#derive(Interpolate)
struct Varying {
uv_inv_z:Vec4, // uv / z
inv_z:f64 // 1 / z
}
したがって、uvに適当な変形を加えていく。
code:rs
fn fragment(_p:&Point2, uni:&Uniform, vary:&Varying) -> Vec4 {
let (w, h) = uni.texture.dimensions();
let z = 1. / vary.inv_z;
let uv = &vary.uv_inv_z * z;
let (u, v) = (uv.x() * w as f64, uv.y() * h as f64);
let (u, v) = (u as u32, v as u32);
...
}
code:rs
fn vertex(uni:&Uniform, vert:&Attribute) -> (Varying, Vec4Screen) {
let v = Vec4Project::new(&uni.pvm * &vert.point);
(
Varying{
uv_inv_z: vert.uv.clone() / vert.point.z(),
inv_z: 1. / v.w()
},
v.into_screen(&uni.size)
)
}
ちょっと定義を書き換える。
code:rs
pub trait RenderingPipeline<
Uniform,
Attribute,
Varying,
where Varying: Interpolable
{
fn vertex(uni:&Uniform, vert:&Attribute) -> (Varying, Vec4Screen);
fn fragment(p:&Point2, uni:&Uniform, vary:&Varying) -> Vec4;
}
さて、これをMeshRenderer側に適応させていこうappbird.icon
まず、都合上Meshの設計をStruct of ArrayからArray of Structureにする
これは添え字アクセスのしやすさ、要素数がそろうこと、型周りの管理が容易になること、ECSの場合とは違いSoAにしてもキャッシュの恩恵がそれほど大きくないことなどが要因として挙げられる。
code:rs
struct VertexArrayObject<Attribute> {
attribute:Vec<Attribute>,
idx:Vec<usize>
}
そうすると、MeshRenderer側はR:RenderingPipelineを用いて、次のように書ける。
R::Attribute, R::Uniformは外部から受け取ることにする。
code:mesh_renderer.rs
impl<R> MeshRenderer<R> where R:RenderingPipeline {
pub fn new(rp:R) -> Self {
MeshRenderer { culling: false, rp }
}
pub fn render(
&self,
canvas:&mut Canvas,
mesh:&VertexArrayObject<R::Attribute>,
uni:&R::Uniform,
) {
まず頂点シェーダーに通す。VAOの属性配列をひとつひとつRの関連関数vertexに渡せばOK
code:mesh_renderer.rs
// 頂点シェーダー
let vert_out:Vec<(<R as RenderingPipeline>::Varying, Vec4Screen)> =
mesh.attribute.iter()
.map(|attr| { R::vertex(uni, attr) } ).collect();
結果はVec<(Varying, Vec4Screen)>として与えられるので、インデックスバッファオブジェクトに従って頂点配列に整列しなおす
code:rs
let polygons =
mesh.idx.iter()
.map(|poly| { poly.map(|idx| { &vert_outidx.0 }) });
let varyings =
mesh.idx.iter()
.map(|poly| { poly.map(|idx| { &vert_outidx.1.0 }) });
そしてさらにpolygonsとvaryingsを並列で舐めて、描画すべき範囲とBarycentric座標を求める準備をする。
code:rs
for (varying, points) in polygons.zip(varyings) {
// y基準でソート
let (x_segment, y_segment, z_segment) = drawing_segments(canvas, &points);
if z_segment.is_empty() { return; }
// Barycentric座標
let area_abc = area(&points0, &points1, &points2);
// culling
if self.culling && area_abc < 0. { return; }
if area_abc.abs() < 1e-6 { return; }
let inv_abc = 1./area_abc;
for y in &y_segment {
for x in &x_segment {
...
}
}
}
xyループの中ではBarycentric座標を求め、補間しつつ各フラグメントごとにフラグメントシェーダーを走らせればOK。
補間には R::Varying::interpolateを使えばよい。
code:rs
let p = Vec4::newpixel(x, y);
let w = [
area(&points1, &points2, &p) * inv_abc,
area(&points2, &points0, &p) * inv_abc,
area(&points0, &points1, &p) * inv_abc,
];
let u_intv = 0. .. 1.;
if !w.iter().all(|e| u_intv.contains(e)) { continue; }
let p = p.to_point2();
let frag_vary = R::Varying::interpolate(&varying, &w);
let color = R::fragment(&p, uni, &frag_vary);
let depth = w0*points0.z() + w1*points1.z() + w2*points2.z();
canvas.draw_pixel_with_depth(&p, &depth, &color);
ではUniformはどこから与える?appbird.icon
renderが呼び出されるのはWorldだが、シェーダーのUniformが変わるたびにWorldを書き換えなくてはならないのはちょっと不便だ...appbird.icon
たいていCamera, Textureなどであることが多いので、できればActor側に責任を押し付けたい。
でもそのためにはUniformを用意するためにあらかじめ何の情報が必要なのかを列挙しきれる必要がある。Worldの情報で与えなければいけないものってあるか?
Cameraの情報とLightぐらいかなぁ...appbird.icon
Cameraは渡すとして、LightはActorに持ってもらえればよいのではないだろうか。
逆に、WorldからはCameraぐらいしか与えられるものが無いんじゃないか?
Lightがあるときどう設計するかはかなり難しそうだが...。
actor.renderが呼び出しの間に挟まるので、そこでuniform変数を用意するようにしよう。
というわけでね、まず初期化
まずplaneをVertexArrayObjectの枠組みにはめなおす
code:rs
type TextureUniform = <TexturePipeline as RenderingPipeline>::Uniform;
type TextureAttribute = <TexturePipeline as RenderingPipeline>::Attribute;
pub fn plane_mesh() -> VertexArrayObject<TextureAttribute> {
let vectors = vec![
Vec4::newpoint(0., -1., -1.),
Vec4::newpoint(0., -1., 1.),
Vec4::newpoint(0., 1., 1.),
Vec4::newpoint(0., 1., -1.),
];
let indecies: Vec<usize; 3> = vec![
0, 1, 2,
2, 3, 0,
];
let uv: Vec<Vec4> = vec![
Vec4::newpoint(0., 0., 0.),
Vec4::newpoint(0., 1., 0.),
Vec4::newpoint(1., 1., 0.),
Vec4::newpoint(1., 0., 0.),
];
VertexArrayObject::<_> {
attribute: vectors.into_iter().zip(uv).map(|(e, v)| TextureAttribute{ point: Vec4Model(e), uv:v }).collect(),
idx: indecies
}
}
textureにはReference Countを持たせることに
Uniform変数に渡すたびにカウントが増えるよ
多分頑張ればLifetimeを指定することで解決しそうだけど、TexturePipelineのほうにもlifetimeの指定が必要になったりと定義が大変なことになったので...
ここどうすればいいんだろう、教えて有識者
code:world.rs
#derive(Clone)
pub struct PlaneActor {
pub mesh:VertexArrayObject<TextureAttribute>,
pub transform:Transform,
pub renderer: MeshRenderer<TexturePipeline>,
pub texture:Rc<Rgba32FImage>
}
impl PlaneActor {
pub fn new() -> Self {
let texture = ImageReader::open("resource/texture.jpg").unwrap().decode().unwrap();
let texture = Rc::new(texture.to_rgba32f());
Self {
mesh: plane_mesh(),
transform: Transform::new(),
renderer: MeshRenderer::new(TexturePipeline{} ),
texture
}
}
}
そしてRcをcloneしたり、行列を作ったりしてuniform変数に渡す。
code:rs
impl Actor for PlaneActor {
...
fn render(&self, _camera:&Camera, canvas:&mut Canvas, pv:&Mat4x4) -> () {
let uni = TextureUniform{
pvm: pv * &self.transform.model_conversion(),
size: canvas.size(),
texture: self.texture.clone(),
};
self.renderer.render(canvas, &self.mesh, &uni);
}
...
}
元の「Colorとpointのデータでtetrahedronを描くやつ」のプログラムも対応させようappbird.icon
というわけで、まずはレンダリングパイプラインをこしらえていく...。
ColorPipeline
Fogの設定も織り交ぜるようにすると...
code:shader/color_pipeline.rs
struct Uniform {
pub pvm:Mat4x4,
pub size:Point2,
pub background_color:Vec4,
pub far:f64,
pub near:f64,
pub fog_far:f64,
}
#derive(Clone)
pub struct Attribute {
pub point:Vec4Model,
pub color:Vec4,
}
#derive(Interpolate)
struct Varying {
color: Vec4,
inv_z:f64,
}
そのうえでVertexはそれに合うように定義していく
code:rs
fn vertex(uni:&Uniform, vert:&Self::Attribute) -> (Varying, Vec4Screen) {
let v = Vec4Project::new(&uni.pvm * &vert.point.0);
(
Varying{
color: vert.color.clone(),
inv_z: 1./ v.w()
},
v.into_screen(&uni.size)
)
}
fragmentはフォグ色とのブレンドを意識して実装
code:rs
fn fragment(_p:&Point2, uni:&Uniform, vary:&Varying) -> Vec4 {
let far = uni.far;
let near = uni.near;
let fog_far = uni.fog_far;
let z_value = 1./vary.inv_z;
let depth = (2.*far*near) / (far+near - (far-near)*z_value);
let depth = (depth - near)/(fog_far - near);
let depth = f64::clamp(depth, 0.0, 1.0);
let fog = depth*depth;
return &uni.background_color*fog + &vary.color*(1. - fog);
}
tetrahedron_meshではVertexArrayObjectを生産するようにする。
code:rs
pub fn tetrahedron_mesh(vert_color:Vec4; 4) -> VertexArrayObject<ColorAttr> {
...
VertexArrayObject::<_>{
attribute: vertices.into_iter().zip(colors)
.map(|(point, color)| { ColorAttr{ point, color } })
.collect(),
idx: v_idx_list
}
}
よし!これで描画方法を変えたければRenderingPipelineだけに注目すればよくなったぞ!appbird.icon
テクスチャのほうを試してみよう 正しく動くかな?
今、プロジェクトが複数個に分かれているので...
default-runを追記
code:rs
package
name = "renderer_core"
version = "0.1.0"
edition = "2021"
default-run = "renderer_core"
https://gyazo.com/5de763e4c107e2547dc9f6d78ff0449b
えぇぇぇ.............appbird.icon
冷静に考えると、
対応関係がおかしい... w値の補間がうまくいっていない
でもtetrahedronだと正常に色が補間されている
視点によってぐにゃったりする ---> z値の補間がうまくいっていない?
v.wから出力してみたが、depth自体は正しく取れていそう。
depthからuvをどう計算するかが問題そうかな...。
なんかおかしくないか?
code:rs
uv_inv_z: vert.uv.clone() / vert.point.0.z(),
このz値モデル座標でのz値だぞ!!
code:rs
let v = Vec4Project::new(&uni.pvm * &vert.point.0);
...
uv_inv_z: vert.uv.clone() / v.w()
はい。。。。appbird.icon
変換後の座標を使いましょう
https://gyazo.com/93940f985604721bab067510b16325f4
よくよく考えるとtetrahedronの出力結果もフォグが反映されなくなってる...なぜ?appbird.icon
z-valueの補間はうまくいっていそう。フォグを遠くにかかるようにしすぎたかな
あぁ、そもそもz-valueを取れてるんだから、透視深度からわざわざ元に戻す必要ないじゃん
farはもう必要ないので、Uniのパラメータから外しておく
https://gyazo.com/e44936d677fe2f7503d86ed0acc37679
OK、問題なさそう!appbird.icon