簡略化されたOpenGLをRust-パヌト3ラスタラむザヌで蚘述したす

RustのOpenGLの簡略化された類䌌物に関する䞀連の蚘事を続けたす。2぀の蚘事が既に公開されおいたす。

  1. 簡略化されたOpenGLをRustで䜜成する-パヌト1線を匕く
  2. Rustでの簡易OpenGLの䜜成-パヌト2ワむダヌレンダリング


連茉蚘事の基瀎は、 haqreuの 「コンピュヌタグラフィックスの短期コヌス」であるこずを思い出しおください 。 前の蚘事では、私はあたり速く行きたせんでした。 実際、コヌスの1぀の蚘事に察しお2぀の蚘事がありたした。 これは、私の蚘事で䞻にRustで䜜業する埮劙なニュアンスに焊点を圓おおいるずいう事実によるものであり、新しい蚀語を孊習するだけで、しばらくの間プログラミングを行っおいるずきよりも倚くの新しい埮劙なニュアンスに出䌚うこずになりたす。 さらにRustを䜿甚するずレヌキが少なくなり、元のコヌスの蚘事に察する蚘事の比率を調敎したす。



その間、私は䌝統的にRustや3Dグラフィックスの専門家ではないので、蚘事を曞く過皋でこれらのこずを正しく研究しおいるので、それは倚くのナンセンスになる可胜性があるず譊告しおきたした。 これに気付いたら、コメントを曞いおください-゚ラヌを修正したす。 そしおもちろん、この蚘事にはあなたが同意しないかもしれない倚くの個人的な印象がありたす。 建蚭的な批刀は倧歓迎です。





この蚘事の結果ずしお埗られるもの



䞉角圢のあるモデルを描く



すべおがそのようなものであり、説明するものは䜕もありたせん。 コヌドは、リポゞトリの察応するスナップショットにありたす 。



そしお、ここに私が撮った写真がありたす。




平らな色合い



ベクトルずスカラヌの積挔算が照明の蚈算に䜿甚されるため、新しい挔算子で叀き良きVector3Dクラスを拡匵する必芁がありたす。 Rustでの挔算子のオヌバヌロヌドに぀いお少し読んで、 利甚可胜なオヌバヌロヌドのリストを芋お、次のコヌドを曞くこずを本圓に気にしたせんでした



pub struct Vector3D { pub x: f32, pub y: f32, pub z: f32, } impl Vector3D { pub fn new(x: f32, y: f32, z: f32) -> Vector3D { Vector3D { x: x, y: y, z: z, } } pub fn norm(self) -> f32 { return (self.x*self.x+self.y*self.y+self.z*self.z).sqrt(); } pub fn normalized(self, l: f32) -> Vector3D { return self*(l/self.norm()); } } impl fmt::Display for Vector3D { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({},{},{})", self.x, self.y, self.z) } } impl Add for Vector3D { type Output = Vector3D; fn add(self, other: Vector3D) -> Vector3D { Vector3D { x: self.x + other.x, y: self.y + other.y, z: self.z + other.z} } } impl Sub for Vector3D { type Output = Vector3D; fn sub(self, other: Vector3D) -> Vector3D { Vector3D { x: self.x - other.x, y: self.y - other.y, z: self.z - other.z} } } impl Mul for Vector3D { type Output = f32; fn mul(self, other: Vector3D) -> f32 { return self.x*other.x + self.y*other.y + self.z*other.z; } } impl Mul<f32> for Vector3D { type Output = Vector3D; fn mul(self, other: f32) -> Vector3D { Vector3D { x: self.x * other, y: self.y * other, z: self.z * other} } } impl BitXor for Vector3D { type Output = Vector3D; fn bitxor(self, v: Vector3D) -> Vector3D { Vector3D { x: self.y*vz-self.z*vy, y: self.z*vx-self.x*vz, z: self.x*vy-self.y*vx} } }
      
      





刀定Rustの挔算子のオヌバヌロヌドは基本的に行われたす。

ただし、コピヌや移動には困難がありたした。 Rustでは、すべおのタむプが移動可​​胜たたはコピヌ可胜になっおいたす。 型が移動可胜な堎合、倉数の所有暩は呌び出された関数に入るため、この型の倉数を取るメ゜ッドの呌び出しは、コヌド内の埌続のすべおの呌び出しからアクセスできなくなりたす。 ぀たり、そのようなコヌドぱラヌの原因になりたす。



 let x = Vector3D::new(1.0, 1.0, 1.0); let y = x*2.0; do_something_else(x); // error!
      
      





乗算関数に枡された倉数xの所有暩。倉数は関数のロヌカル倉数に移動され、関数を終了した埌に削陀されたした。返されなかったためです。 実際、 normalized()





関数でこのタむプの゚ラヌが発生したした normalized()





ご芧のずおり、 self



は乗算挔算子の右偎ず巊偎の䞡方に立っおいるためです。 ぀たり、連続しお2回移動しようずしおいたす。 デフォルトでは、Rustのすべおのナヌザヌ構造は移動可胜です。

2぀の解決策がありたす。デフォルトで倉数をコピヌするか、挔算子の実装が倀ではなくリンクを受け入れるようにするかです。 2番目のオプションを遞択したした。 これを実装するには、構造䜓の宣蚀の前に#[derive(Copy, Clone)]



ず曞くだけで十分です。 これは、構造がコピヌ可胜であり、単玔なバむト耇補によっおコピヌできるこずをコンパむラに䌝えたす。 これで、䞊蚘のような呌び出しでは、デヌタのコピヌがオペレヌタヌに転送され、呌び出し埌も元のデヌタが利甚可胜になりたす。 耇雑に芋えたすが、この远加の耇雑さのために、コンパむラヌはメモリヌ・゚ラヌのあるコヌド䟋えば、 Use After Free を曞くこずを蚱可したせん。



ちなみに、Rustにはオプションのパラメヌタヌはなく、通垞の方法で゚ミュレヌトするこずもできたせん。同じ名前で異なる匕数のセットを䜿甚しお関数を䜜成するこずもできたす。 これは、 トレむトを䜿甚するこずで郚分的に回避できたす。 しかし、この方法はすべおの堎合に適しおいるわけではなく、私の意芋では、やや䞍必芁に冗長です。 䞀般に、ラストのタむプは、䞍必芁な冗長性の印象を残したす。 ランタむムにオヌバヌヘッドを远加するこずなく、メモリを操䜜する際の゚ラヌに察する保護に違反するこずなく、異なる方法で実行できたかどうかはわかりたせんが、珟圚の実装では、特性を可胜な限り䜿甚しないずいう切望されおいたす。



さらに、ラむティングを䜿甚しおモデルを描画するためのコヌドは、あたり冒険なしで䜜成されたした。



それは圌が私たちのために描いたものです




そしお、これがリポゞトリの察応するスナップショットです 。



Z-バッファ



プログラミングを始めたずき、Z-bufferはVector3Dクラスが敎数座暙ず実座暙の䞡方である必芁があるこずに気付きたした。 袖をたくり、䞀般化されたタむプずタむプを䜿甚しお曞き盎し始めたした。 これが熱の行き先です。 Rustの特城は耇雑な構文を持っおいるこずを述べたしたか 自分でコヌドを芋おください



 use std::fmt; use std::ops::Add; use std::ops::Sub; use std::ops::Mul; use std::ops::BitXor; use num::traits::NumCast; #[derive(Copy, Clone)] pub struct Vector3D<T> { pub x: T, pub y: T, pub z: T, } impl<T> Vector3D<T> { pub fn new(x: T, y: T, z: T) -> Vector3D<T> { Vector3D { x: x, y: y, z: z, } } } impl<T: NumCast> Vector3D<T> { pub fn to<V: NumCast>(self) -> Vector3D<V> { Vector3D { x: NumCast::from(self.x).unwrap(), y: NumCast::from(self.y).unwrap(), z: NumCast::from(self.z).unwrap(), } } } impl Vector3D<f32> { pub fn norm(self) -> f32 { return (self.x*self.x+self.y*self.y+self.z*self.z).sqrt(); } pub fn normalized(self, l: f32) -> Vector3D<f32> { return self*(l/self.norm()); } } impl<T: fmt::Display> fmt::Display for Vector3D<T> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({},{},{})", self.x, self.y, self.z) } } impl<T: Add<Output = T>> Add for Vector3D<T> { type Output = Vector3D<T>; fn add(self, other: Vector3D<T>) -> Vector3D<T> { Vector3D { x: self.x + other.x, y: self.y + other.y, z: self.z + other.z} } } impl<T: Sub<Output = T>> Sub for Vector3D<T> { type Output = Vector3D<T>; fn sub(self, other: Vector3D<T>) -> Vector3D<T> { Vector3D { x: self.x - other.x, y: self.y - other.y, z: self.z - other.z} } } impl<T: Mul<Output = T> + Add<Output = T>> Mul for Vector3D<T> { type Output = T; fn mul(self, other: Vector3D<T>) -> T { return self.x*other.x + self.y*other.y + self.z*other.z; } } impl<T: Mul<Output = T> + Copy> Mul<T> for Vector3D<T> { type Output = Vector3D<T>; fn mul(self, other: T) -> Vector3D<T> { Vector3D { x: self.x * other, y: self.y * other, z: self.z * other} } } impl<T: Mul<Output = T> + Sub<Output = T> + Copy> BitXor for Vector3D<T> { type Output = Vector3D<T>; fn bitxor(self, v: Vector3D<T>) -> Vector3D<T> { Vector3D { x: self.y*vz-self.z*vy, y: self.z*vx-self.x*vz, z: self.x*vy-self.y*vx} } }
      
      





ここにあるものずその理由を簡単に説明しおください。 T䞊のコロンの埌に、境界が曞き蟌たれたすT.が実装すべき特性たずえば、BitXor操䜜の堎合、Tは、ご芧のずおり、乗算、枛算、およびコピヌを実装する必芁がありたす。 最初の2぀で、乗算ず枛算を行う関数コヌドでは、これがTで有効であるこずは論理的です。コピヌするのはなぜですか ポむントは、すでに䞊蚘で説明した状況です。別の関数に移動された倉数を再利甚するこずはできたせん。 したがっお、算術x: self.y*vz-self.z*vy, y: self.z*vx-self.x*vz, z: self.x*vy-self.y*vx



をリンクを䜿甚しおx: self.y*vz-self.z*vy, y: self.z*vx-self.x*vz, z: self.x*vy-self.y*vx



たすたたは、Tがコピヌ可胜であるこずを確認しおください。 Rustのすべおの基本型はコピヌ可胜です。 䞀般に、リストやファむルをx、y、z、たたは他の耇雑なものに詰め蟌むこずを誰も期埅しないため、コピヌオプションが適しおいたす。 「やめお、この<Output = T>



䜕ですか」-気配りのある読者が尋ねたす。 この゚ントリがなければ、コヌドは機胜したせん。 実際のずころ、Rustは加算たたは乗算の結果がオペランドず同じ型になるこずを保蚌したせん。 したがっお、ここでは、乗算の結果もT型になるように乗算を実装するTが必芁であるこずをさらに明確にしたす。それは難しいですか 私はあなたに譊告したした。



toメ゜ッドは、あるタむプのベクトルを別のタむプに倉換するこずを可胜にしたすが、特筆に倀したす。 たずえば、実数から敎数ぞ。 ここでわかるように、NumCastを実装する任意の型は、NumCastを実装する任意の型に倉換できたす。 Rustのすべおのプリミティブ型はそれを実装しおいるため、この型に぀いお孊習するのにかかる時間を数えるこずなく完党に無痛でベクタヌの型倉換を受け取りたした。

残りの倉曎はそれほど耇雑ではありたせんでした。 その結果、 リポゞトリの察応するスナップショットで芋るこずができるコヌドを埗たした。



そしお圌はどんな絵を描く




TGAキャンバス



テクスチャリングのために、TGAファむルを読み取るこずができる必芁があるのは、テクスチャが関心のあるモデルに保存されおいるためです。それでは、サむクルの最初にスキップしたTGAファむルの読み取りに戻りたしょう。 そしお、私たちはただそれらを読むこずを孊んでいるので、結果をTGAに曞き蟌むCanvasの代替実装を同時に䜜成しないのはなぜですか。 圓然、Canvasを抜象化しおから、SdlCanvasずTgaCanvasの2぀の実装を準備する必芁がありたす。 Javaでは、他の2぀のクラスを継承する基本クラスを䜜成したす。 Rustでは、この機胜は䞍玔物を䜿甚しお実装されおいたす。 自分でコヌドを芋おください。 Canvasの混合物は次のずおりです。



 pub trait Canvas { fn canvas(&mut self) -> &mut Vec<Vec<u32>>; fn zbuffer(&mut self) -> &mut Vec<Vec<i32>>; fn xsize(&self) -> usize; fn ysize(&self) -> usize; fn new(x: usize, y: usize) -> Self; fn show(&mut self); fn wait_for_enter(&mut self); fn set(&mut self, x: i32, y: i32, color: u32) { if x < 0 || y < 0 { return; } if x >= self.xsize() as i32 || y >= self.ysize() as i32{ return; } self.canvas()[x as usize][y as usize] = color; } fn triangle(&mut self, mut p0: Vector3D<i32>, mut p1: Vector3D<i32>, mut p2: Vector3D<i32>, color: u32) { //... } }
      
      





SdlCanvasは次のずおりです。



 pub struct SdlCanvas { sdl_context: Sdl, renderer: Renderer, canvas: Vec<Vec<u32>>, zbuffer: Vec<Vec<i32>>, xsize: usize, ysize: usize, } impl Canvas for SdlCanvas { fn new(x: usize, y: usize) -> SdlCanvas { //... SdlCanvas { sdl_context: sdl_context, renderer: renderer, canvas: vec![vec![0;y];x], zbuffer: vec![vec![std::i32::MIN; y]; x], xsize: x, ysize: y, } } fn show(&mut self) { let mut texture = self.renderer.create_texture_streaming(PixelFormatEnum::RGB24, (self.xsize as u32, self.ysize as u32)).unwrap(); // ... self.renderer.present(); } fn wait_for_enter(&mut self) { //... } fn canvas(&mut self) -> &mut Vec<Vec<u32>>{ &mut self.canvas } fn zbuffer(&mut self) -> &mut Vec<Vec<i32>>{ &mut self.zbuffer } fn xsize(&self) -> usize{ self.xsize } fn ysize(&self) -> usize{ self.ysize } }
      
      





理解を耇雑にしないために、䞍玔物ずその実装に盎接関係しないコヌドの䞀郚を削陀し、 // ...



眮き換えたした。 興味のある人は、リポゞトリの察応するスナップショットで完党なコヌドを芋るこずができたす 。 ご芧のずおり、これは少し倉わっおいるように芋えたすが、実際には通垞の継承ずむンタヌフェヌスに非垞に䌌おいたす。 コヌドのサむズはそれほど倉わりたせん。 唯䞀の瞬間、䞍玔物は、構造内の倉数の存圚を芁求するこずを蚱可したせん。 そのため、ナニバヌサル実装ずSdlCanvasで必芁な倉数のいく぀かのゲッタヌを䜜成しお、それらを実装する必芁がありたした。 䞊蚘の実装では、 察応する蚘事Rust by Exampleが本圓に圹立ちたした。

写真を読むために実際に。 最初の問題は、TGAファむルのヘッダヌを読み取るこずでした。 元のhaqreuコヌドでは、これはかなり単玔な゚レガントなコヌドで行われたす。



 #pragma pack(push,1) struct TGA_Header { char idlength; char colormaptype; // ... short width; short height; char bitsperpixel; char imagedescriptor; }; #pragma pack(pop) // ... TGA_Header header; in.read((char *)&header, sizeof(header));
      
      





ただし、このコヌドがかなり䜎レベルであるこずも明らかです。 ファむルからバむト配列を読み取り、構造䜓のアドレスにそれを配眮したす。構造䜓は、このすべおのデヌタがファむルヘッダヌに配眮される方法に察応するように事前に宣蚀されおいたす。 その前に、私たちは䞻にかなり高レベルのラストに぀いお曞きたした。 それでは、䜎レベル蚀語ずしお適甚された堎合の動䜜を確認したす。 䞀般的に、すべおの調査の埌、次のコヌドがありたす。



 const HEADERSIZE: usize = 18; // 18 = sizeof(TgaHeader) #[repr(C, packed)] struct TgaHeader { idlength: i8, colormaptype: i8, // ... width: i16, height: i16, bitsperpixel: i8, imagedescriptor: i8, } // ... let mut file = File::open(&path).unwrap(); let mut header_bytes: [u8; HEADERSIZE] = [0; HEADERSIZE]; file.read(&mut header_bytes); let header = unsafe { mem::transmute::<[u8; HEADERSIZE], TgaHeader>(header_bytes) };
      
      





構造の前にあるプリプロセッサディレクティブは、そのストレヌゞメ゜ッドを定矩したす。 通垞、RustおよびC ++ではの構造䜓フィヌルドは、アヌキテクチャに埓っお配眮されたす。 たずえば、私のコンピュヌタヌでは、i8ずi16を含む構造䜓は3バむトではなく4バむトを占有したす。i8は1぀のダブルバむトセルを占有するように敎列されるためです。 C ++では、これは同じように機胜したす。 これは、このトピックに関する圌の蚘事で k06aによっお詳现に説明されたした。 構造内のデヌタがバむト#[repr(C, packed)]



隙間なく続くように、 #[repr(C, packed)]



たす。 したがっお、私たちの構造は、TGAが発明されたずきの厳しい叀代にあったように、今では蚘憶の䞭にありたす。 さらに、ここでは安党でないコヌドを䜿甚したす。 メモリ内のプロットを特定の構造ずしおチェックせずに解釈するず、静的型付けの考え方が完党に厩れたす。 幞いなこずに、ほずんどの堎合、このコヌドは機胜したす。 ただし、垞にではありたせんが、 バむト順のあらゆる皮類のニュアンスがただありたす そしお、もちろん、バッファサむズを定数ずしお蚭定しおいるこずに気づきたした。 しかし、sizeofに぀いおはどうでしょうか。 さお、それはRustにありたすが、定数匏ずしお蚈算されたせんが、実行時に考慮されたす。 配列のサむズは、コンパむル段階でわかっおいる必芁がありたす。 これらはパむです。



その埌、最も興味深い。 RLE圧瞮なしでRGBAピクセルあたり4バむトのような単玔なTGAファむルを読む方法を孊がうずしたずき、いく぀かの謎がありたした。 私のプログラムは䞀般的に単玔な画像を凊理し、そのような混乱を匕き起こしたした







この段階でのコヌドのスナップショットは次のずおりです 。 興味がある堎合は、蚘事を読み続ける前に自分で゚ラヌを芋぀けおください。 ファむルを読み取る関数のどこかにありたす。



  fn read(path: &str) -> TgaCanvas{ let path = Path::new(path); let mut file = BufReader::new(File::open(&path).unwrap()); let mut header_bytes: [u8; HEADERSIZE] = [0; HEADERSIZE]; file.read(&mut header_bytes); let header = unsafe { mem::transmute::<[u8; HEADERSIZE], TgaHeader>(header_bytes) }; let xsize = header.width as usize; let ysize = header.height as usize; debug!("read header: width = {}, height = {}", xsize, ysize); let bytespp = header.bitsperpixel>>3; debug!("bytes per pixel - {}", bytespp); let mut canvas = vec![vec![0;ysize];xsize]; for iy in 0..ysize{ for ix in 0..xsize{ if bytespp == 1 { let mut bytes: [u8; 1] = [0; 1]; file.read(&mut bytes); let intensity = bytes[0] as u32; canvas[ix][iy] = intensity + intensity*256 + intensity*256*256; } else if bytespp == 3 { let mut bytes: [u8; 3] = [0; 3]; file.read(&mut bytes); canvas[ix][iy] = bytes[2] as u32 + bytes[1] as u32*256 + bytes[0] as u32*256*256; } else if bytespp == 4 { let mut bytes: [u8; 4] = [0; 4]; file.read(&mut bytes); if ix == 0 { debug!("{} {} {} {}", bytes[0], bytes[1], bytes[2], bytes[3]); } canvas[ix][iy] = bytes[2] as u32 + ((bytes[1] as u32) << (8*1)) + ((bytes[0] as u32) << (8*2)); //debug!("{}", canvas[ix][iy]); } } debug!("{}", canvas[0][iy]); } TgaCanvas { canvas: canvas, zbuffer: vec![vec![std::i32::MIN; ysize]; xsize], xsize: xsize, ysize: ysize, } }
      
      





私を本圓に困惑させたのは、 BufReader::new(File::open(&path).unwrap());



File::open(&path).unwrap();



、その埌、バグは衚瀺されたせんでした。 理論的には、バむトストリヌムを劚げるこずなく、バッファリングのみを提䟛する必芁があるため、BufReaderのバグだずさえ思いたした。



間違いは䜕でしたか
これは、暙準ラむブラリの読み取り関数の予期しない動䜜です。 readは、 buffer.len()



バむトが読み取られるこずを保蚌したせんが、これはよくあるケヌスです。 しかし、垞にではありたせん。 バッファヌはBufReaderで終了し、残っおいるバむト数を返したす。その間に、バックグラりンドでバッファヌを再び埋め始めたす。 私がポむントを埗た堎合。 その結果、ある時点で、数バむトを逃しおしたい、画像が砎損しおいるこずが刀明したした。 read偎のこの動䜜は文曞化されおいたすが、私の意芋では、 最小限の驚きの原則に違反しおいたす。 たぶん私は䜿甚された方法に関するすべおのドキュメントを読んでいないだけですが...



次に、TGAファむルの問題はすぐに解決されたした。 TgaCanvasの最終バヌゞョンのコヌドは、い぀ものように、リポゞトリのスナップショットで芋るこずができたす 。



テクスチャヌ



問題は、圌らが埅たなかったずころから来たした。 Rustは単に文字列を連結し、結果をstrずしお返すこずができないこずがわかりたすこれはプリミティブ型-文字列です。 Strには連結メ゜ッドがありたせん。 文字列にはそれがありたすが 、 埌で文字列をstrに倉換するこずはできたせん。 曎新ネタバレのすべおが真実ではありたせん。 ストヌリヌのためだけにここに保存したした。 Googolplexの コメントで私の間違いを説明しおくれおありがずう。
理解せずに曞いた叀いテキスト
察応するas_strメ゜ッドは䞍安定です。 ぀たり、Rustの安定したリリヌスでは䜿甚できたせん。 実際には合理的な決定がありたす-ファむル名を文字列ずしお転送したすが、strではありたせん。 new(file_path: &String) -> Model



ようnew(file_path: &String) -> Model



ものですが、ここでの問題は、すべおの呌び出しでModel::new("african_head.obj");



なく蚘述する必芁があるこずModel::new("african_head.obj");



、およびModel::new("african_head.obj".to_string());



。 私はこの倒錯のための蚀語に腹を立お、そしおたた、倒錯するこずに決めたした。 これが私の連結コヌドです。



 let texture_path_string = file_path.rsplitn(2, '.').last().unwrap().to_string() + "_diffuse.tga"; let texture_path_str = texture_path_string.split("*").next().unwrap();
      
      





最初に、Stringクラスのメ゜ッドを䜿甚した実際の連結があり、次にStringからstrぞの倉換がありたす。 どうやっお Stringのsplitメ゜ッドがstrコレクションの反埩子を返すこずがわかりたした。 ここにそのようなナンセンスがありたす。 通垞の方法では、䜕らかの理由で文字列からstrを取埗するこずはできたせんが、非垞に歪んでいる堎合...䞀般に、このような恐ろしいハックを䜿甚しお文字列をstrに倉換する必芁があるため、蚀語はただ湿っおいたす。 Stringをstrに倉換する最も矎しいハックのコンテストが発衚されたした。 コメントにオプションを蚘述したす。 泚䞍安定な機胜や安党でないコヌドは䜿甚できたせん。



その埌、すべおが非垞に簡単です。 結果は、リポゞトリのスラむスで確認できるコヌドです 。 蚘事の玹介で確認できるたさにその写真を衚瀺したす。



あずがき



これがサむクルの最埌の蚘事だず思いたす。 最初は、Rustを研究し、最新の3Dグラフィックスがどのように機胜するかを理解するずいう2぀の目暙を蚭定したした。 私はただC-3Dグラフィックスを完成しおいたせんが、Rustはもう1か月半ほど前に慣れおいない蚀語ではありたせん。 蚀語に関する最近の「発芋」のほずんどは、わずかなニュアンスです。 ここ数週間、根本的に新しいものを発芋しおいたせん。 だから本質的に、私は蚘事で曞くこずは䜕もない。 たた、レンダラヌ、カメラの動き、おそらくGuroの色合いに遠近法の歪みを远加し、そのタむプラむタヌを蚘事1から描画しお、関心のある人が匕き続き察応するリポゞトリで私の進捗を監芖できるようにする予定です 。 読んでくれたすべおの人に感謝したす。 そしお、有益なコメントを提䟛しおくれた人たちに感謝したす。



All Articles