ぷるぷるの雑記

低レイヤーがんばるぞいなブログ. 記事のご利用は自己責任で.

Blenderで調べたことのメモ- その1 -

自分用のメモ兼リンク集みたいな感じ.

UV展開

一般的には画像の位置を決める作業といわれるが、おそらくは逆で頂点属性のうちテクスチャ座標を決めるという作業(だと思う)。

したがってUnityに3Dモデルを持ってきたときにうまくテクスチャが反映されていなくてもテクスチャ座標自体は反映されているのでそこまで問題はない(はず).

頂点ペイント

頂点属性のうち頂点カラーを編集することが出来る。

左上のObject ModeやEdit Modeを切り替えるところから選ぶことが出来る.

実際のところオブジェクトの色にはテクスチャを利用すればよく、Blenderでわざわざ頂点カラーを編集するときは単なる3チャンネルの情報として利用することが多い.

頂点ペイントのやりかた

vtuberkaibougaku.site

頂点ペイントの活用方法

tsumikiseisaku.com

tsumikiseisaku.com

tsumikiseisaku.com

サーフェイスシェーダー

プログラマブルシェーダーと言ったらバーテックスシェーダーとフラグメントシェーダーだと思っていたが、Unity独自のサーフェスシェーダーというものがあり、フラグメントシェーダーの代わりとして使うことが出来る.

イメージとしては

フラグメントシェーダー = サーフェスシェーダー + ライティング

ライティングに関しては自分でシェーダーを書くこともできるし、Unityの組み込みライティングモデルを用いることもできる(というか、ライティングを書かずに済むためのサーフェスシェーダーなので、カスタムライティングをするくらいなら最初からフラグメントシェーダーを書きなさいってことか).

組み込みライティングモデルを用いる場合、ライティングに関する頂点属性を渡すだけでいい感じにしてくれる.

サーフェスシェーダーの説明

qiita.com

サーフェイスシェーダーの書き方

light11.hatenadiary.com

docs.unity3d.com

2パスレンダリング

名前の通りシェーダー中にPassを2つ書くことが出来る。1st Passで輪郭のみを描画し、2nd Passで通常通りの描画をするといったようなことが出来る.

qiita.com

nn-hokuson.hatenablog.com

UV展開を矩形で行う

なるべく矩形に近いメッシュをアクティブ状態にしてからUV Mapping->Follow Active QuadsからUV展開すると矩形状にUV展開することが出来る. 一度テクスチャ座標をリセットしてから実行すべし.

blender3d.biz

3Dカーソルを原点に合わせる

Shift + C

WebGL2の基礎を理解する

三角形を描画するスクリプトを通してWebGL2の基礎を理解しましょう。

はじめに

WebGL2とはざっくりいうとJavaScriptからOpenGLの機能を利用することができるJavaScriptAPIです. ブラウザの機能なのでプラグインなどが必要なく、既存のOpenGLのコードをかなり流用できます*1

WebGL2があるからにはWebGL(無印)もあるわけですが、せっかくなら新しい方を使おうということでこの記事ではWebGL(無印)に関しては一切触れません。

また、WebGL2を「完全に理解」するためには広範な知識が必要になります。したがって、この記事でカバーする部分とカバーしない部分を明確にしておきましょう。

この記事で扱うこと

  • シェーダープログラムをコンパイルする方法
  • 頂点バッファーオブジェクト(VBO)の利用方法
  • 頂点配列オブジェクト(VAO)の利用方法

この記事で扱わないこと

  • GLSLの書き方
  • インデックスの頂点バッファオブジェクトの利用方法
  • Canvasを画像化してダウンロードする方法
  • vertexAttribPointer()の引数の詳しい説明

コードの全貌

いきなりですが、以下が完成したコードになります。

<!-- index.html -->

<head>
    <style>
        canvas{
            width: 512px;
            height:512px;
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script src="script.js"></script>
</body>
// script.js

"use strict";

let vertexShaderSource = `#version 300 es

in vec4 position;
in vec4 color;
out vec4 vcolor;

void main() {

   vcolor = color;

  // gl_Positionは必須
  // out変数としての定義不要
  gl_Position = position;
}
`;

let fragmentShaderSource = `#version 300 es

// データ型指定必須
precision highp float;

// フラグメントシェーダーは最終的なout変数を定義する必要あり
in vec4 vcolor;
out vec4 outColor;

void main() {
  outColor = vcolor;
}
`;

function createShader(gl, type, source) {
  let shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (success) {
    return shader;
  }

  console.log(gl.getShaderInfoLog(shader));  // eslint-disable-line
  gl.deleteShader(shader);
  return undefined;
}

function createProgram(gl, vertexShader, fragmentShader) {
  let program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  let success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }

  console.log(gl.getProgramInfoLog(program));  // eslint-disable-line
  gl.deleteProgram(program);
  return undefined;
}

function main() {
  // コンテクスト取得
  let canvas = document.querySelector("canvas");
  let gl = canvas.getContext("webgl2");
  if (!gl) {
    return;
  }
  
  // バーテックスシェーダーとフラグメントシェーダーをコンパイル
  let vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
  let fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
  
  // バーテックスシェーダーとフラグメントシェーダーをプログラムとリンクする
  let program = createProgram(gl, vertexShader, fragmentShader);
  
  /******************************************************************************** */
  gl.bindAttribLocation(program, 0, "position");
  gl.bindAttribLocation(program, 1, "color");
  
 
  const tri1 = [
    // x, y, z 
    1, 1, 0, 
    -1, -1, 0,
    1, -1, 0,
    0, 0.75, 0, 1, 
    0, 0.75, 0, 1,
    // Right Down
    0, 1, 0, 1
  ];
  
  const tri2 = [
    // x, y, z, r, g, b, a
    -1, 1, 0, 
    -1, -1, 0,
    1, 1, 0,
    // r, g, b, a
    // Left Up
    0, 0.5, 0, 1, 
    0, 0.75, 0, 1,
    0, 0.75, 0, 1
  ];

  let vao1 = gl.createVertexArray();
  prepare(gl, program, "position", 3, tri1, 0, 0, vao1);
  prepare(gl, program, "color", 4, tri1, 0, 4*9, vao1);
  
  let vao2= gl.createVertexArray();
  prepare(gl, program, "position", 3, tri2, 0, 0, vao2);
  prepare(gl, program, "color", 4, tri2, 0, 4*9, vao2);
    


  // キャンバスいっぱいをクリップ空間とする
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  // キャンバスクリア
  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT);


  draw(gl, program, vao1);
  draw(gl, program, vao2);
}

function draw(gl, program, vao)
{

  // レンダリングに使用するシェーダープログラムを指定
  gl.useProgram(program);

  // レンダリングするVAOを指定
  gl.bindVertexArray(vao);

  // レンダリング
  let primitiveType = gl.TRIANGLES;
  let first = 0;
  let count = 3;
  gl.drawArrays(primitiveType, first, count);
}

function prepare(gl, program, varName, _size, data, _stride, _offset, vao)
{
  // VBOの設定
  let indexOfVbo = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, indexOfVbo);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
  

  // VAOの設定
  let attributeLocation = gl.getAttribLocation(program, varName);
  gl.bindVertexArray(vao);

  // VBO & VAOの設定
  let size = _size;         
  let type = gl.FLOAT;   
  let normalize = false; 
  let stride = _stride;        
  let offset = _offset;        
  gl.vertexAttribPointer(
      attributeLocation, 
      size, 
      type, 
      normalize, 
      stride, 
      offset
  );
  gl.enableVertexAttribArray(attributeLocation);

}

main();

上のコードの描画結果
割といい感じなグラデーション画像になっていますね。

コード解説 - ユーザー定義関数 -

今回のコードで利用しているユーザー定義関数は次のようになっています。

戻り値の型 関数名
WebGLShader createShader()
WebGLProgram createProgram()
なし prepare()
なし draw()

createShader()

function createShader(gl, type, source) {
  let shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (success) {
    return shader;
  }

  console.log(gl.getShaderInfoLog(shader));  // eslint-disable-line
  gl.deleteShader(shader);
  return undefined;
}

この関数は文字列として定義されたシェーダーをコンパイルして利用可能な型(WebGLShader)としてリターンします。

createProgram()

function createProgram(gl, vertexShader, fragmentShader) {
  let program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  let success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }

  console.log(gl.getProgramInfoLog(program));  // eslint-disable-line
  gl.deleteProgram(program);
  return undefined;
}

この関数はコンパイル済みのシェーダー(WebGLShader)を受け取り最終的に利用可能な型(WebGLProgram)としてリターンします。



createShader()とcreateProgram()はWebGL2 Fundamentalsのサンプルコードを丸写ししました。

webgl2fundamentals.org

parepare()

function prepare(gl, program, varName, _size, data, _stride, _offset, vao)
{
  // VBOの設定
  let indexOfVbo = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, indexOfVbo);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
  

  // VAOの設定
  let attributeLocation = gl.getAttribLocation(program, varName);
  gl.bindVertexArray(vao);

  // VBO & VAOの設定
  let size = _size;         
  let type = gl.FLOAT;   
  let normalize = false; 
  let stride = _stride;        
  let offset = _offset;        
  gl.vertexAttribPointer(
      attributeLocation, 
      size, 
      type, 
      normalize, 
      stride, 
      offset
  );
  gl.enableVertexAttribArray(attributeLocation);

}

最も煩雑な処理を含んだ関数です。

// VBOの設定
let indexOfVbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, indexOfVbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);

この三行でGPU上のメモリにデータが転送されます。注意点ですが、この時点ではまだ GPUは送られてきたデータが何を表しているのを知りません 。なので、このデータが具体的にどのようなデータなのかをGPUに教えてやる必要があります。このとき、生のデータ自体を管理するものを頂点バッファオブジェクト(VBO)といい、このVBOを管理するものを頂点配列オブジェクト(VAO)といいます。

// VAOの設定
let attributeLocation = gl.getAttribLocation(program, varName);
gl.bindVertexArray(vao);

この二行はVAOを利用するための準備をしています。なお、今回のコードはVAOの生成はmain()内で行うようにしています。varNameにはシェーダー中の変数名です。例えば、gl.getAttribLocation(program, "position")とするとこのシェーダープログラム中のpositionという変数の管理番号(実際には0以上のインデックス)が得られます。

// VBO & VAOの設定
  let size = _size;         
  let type = gl.FLOAT;   
  let normalize = false; 
  let stride = _stride;        
  let offset = _offset;        
  gl.vertexAttribPointer(
      attributeLocation, 
      size, 
      type, 
      normalize, 
      stride, 
      offset
  );
  gl.enableVertexAttribArray(attributeLocation);

このコードで メモリ上のデータがどのようなデータなのかをGPUに覚えさせ、VAOを介すことでシェーダープログラムからその情報に参照できるようにします。もう少し具体的に言うと、gl.vertexAttribPointer()のおかげでシェーダープログラム中の変数とGPUのメモリ上の情報が結びつきます。

ありていに言うと、

VBO→GPU上のメモリにおくるデータを含んだ段ボール


VAO→各段ボールにどんな情報がどんな順番で入っているかを記録しておく伝票



といった感じでしょうか。

実は、1つの段ボールにすべての情報を含んでもよいですし、意味のある情報ごとに分けてもよいです。つまり、位置や色を一つのVBOに含めてもよいですし、逆に2つのVBOに分けても良いのです。

今回のコードは実は無駄なことをしていて、一つのVAOに対して位置と色の情報を含んだVBOを二回ずつ生成しています*2。本来であれば位置と色の情報を含んだVBOを一度だけ生成するか、位置の情報を含んだVBOと色の情報を含んだVBOを1度ずつ生成すれば十分なはずです。

無駄のないコードに直してみるのもいいですね。

draw()

function draw(gl, program, vao)
{

  // レンダリングに使用するシェーダープログラムを指定
  gl.useProgram(program);

  // レンダリングするVAOを指定
  gl.bindVertexArray(vao);

  // レンダリング
  let primitiveType = gl.TRIANGLES;
  let first = 0;
  let count = 3;
  gl.drawArrays(primitiveType, first, count);
}

実際に描画を行う関数です。今回のコードではインデックスを用いていないので与えられた3点を用いて三角形を描画します。

コード解説 - main関数 -

webgl2コンテクストを取得

function main() {
  // コンテクスト取得
  let canvas = document.getElementById("canvas");
  let gl = canvas.getContext("webgl2");
  if (!gl) {
    return;
  }
.
.
.

webgl2コンテクストを取得します。以後はOpenGLとほぼ同一の手続きを行います。

シェーダーのコンパイル、リンク

  // バーテックスシェーダーとフラグメントシェーダーをコンパイル
  let vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
  let fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
  
  // バーテックスシェーダーとフラグメントシェーダーをプログラムとリンクする
  let program = createProgram(gl, vertexShader, fragmentShader);

ユーザー定義関数を利用してシェーダーのコンパイルし、実際の描画に利用できるプログラムを生成します。

シェーダー中のin変数とプロクラム上のインデックスを結びつける

 gl.bindAttribLocation(program, 0, "position");
 gl.bindAttribLocation(program, 1, "color");

以降、programにとってattribute[0]と言えばposition、attribute[1]といえばcolorを表すようになります。

VBOに渡す情報を定義

const tri1 = [
    // x, y, z 
    1, 1, 0, 
    -1, -1, 0,
    1, -1, 0,
    // r, g, b, a
    0, 0.75, 0, 1, 
    0, 0.75, 0, 1,
    0, 1, 0, 1
  ];
  
  const tri2 = [
    // x, y, z, r
    -1, 1, 0,         
    -1, -1, 0,
    1, 1, 0,
    // r, g, b, a
    0, 0.5, 0, 1,  
    0, 0.75, 0, 1,
    0, 0.75, 0, 1
  ];

そのまんまです。今回は[位置,位置,位置,色,色,色]という順番で情報が並んでいますが、prepareの引数を変えれば[位置,色,位置,色,位置,色]にも対応することが出来ます.

VAO、VBOの設定

  let vao1 = gl.createVertexArray();
  prepare(gl, program, "position", 3, tri1, 0, 0, vao1);
  prepare(gl, program, "color", 4, tri1, 0, 4*9, vao1);
  
  let vao2= gl.createVertexArray();
  prepare(gl, program, "position", 3, tri2, 0, 0, vao2);
  prepare(gl, program, "color", 4, tri2, 0, 4*9, vao2);

詳しい説明はユーザー定義関数の解説のところを見てください。

描画

  // キャンバスいっぱいをクリップ空間とする
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  // キャンバスクリア
  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT);


  draw(gl, program, vao1);
  draw(gl, program, vao2);

VAOを指定した後描画を行います。

コード解説、以上!

おわりに

後半は力尽きて走り書き気味でしたが、基本的なところだけでもおさえられてよかったです。個人的にはWebGLの理解にはOpenGLの理解が重要だと思いました。OpenGLの教科書としては次の本に一番お世話になりました。



また、OpenGLを管理しているkhronosグループのクイックリファレンスも参考になりました。 https://www.khronos.org/files/webgl20-reference-guide.pdf

あとMDN Web Docsもお世話になりました。 developer.mozilla.org

今後の目標

  • vertexAttribPointer()の引数のメモを書く
  • インデックス情報を用いたオブジェクトの表現
  • Canvasの画像化

*1:関数名やマクロがほぼ一対一対応している

*2:strideとoffsetの使い方を明確にしたかったため

自分的VSコード便利機能まとめ

すべて覚えるのは無理なので、とりあえず自分がよく使うもののメモ。適宜更新するかも。

基礎編

VSCode自体のショートカットというより、Windowsのショートカットが多い

ショートカット 説明
Ctrl + S ファイル上書き保存
Ctrl + Shift + S ファイルを新規保存
Ctrl + N 新規ファイルを開く
Ctrl + R 最近開いたファイルを選択
Ctrl + C 行をコピー
Ctrl + V ペースト
Ctrl + X 行を切り取り
Ctrl + A 全選択
Ctrl + Z Undo
Ctrl + Y Redo
Ctrl + O ファイルを開く
Ctrl + K + O フォルダを開く

タブ、分割画面編

ショートカット 説明
Ctrl + \ エディタを分割. 同じファイルを複数のタブに分けられる
Ctrl + 1 画面が分割されているとき、1番目の画面にカーソルを合わせる
Ctrl + 2 画面が分割されているとき、2番目の画面にカーソルを合わせる
Ctrl + W 現在のタブを消去
Ctrl + Tab タブを切り替え
Ctrl + @ ターミナルを開く
Ctrl + B サイドバー開閉

エディター編

ショートカット 説明
Ctrl + [ 現在の行のインデントを一段上げる
Ctrl + ] 現在の行のインデントを一段下げる
Ctrl + L 現在の行を選択.連続で押すと複数行選択可能
Fn + ← 行頭にカーソルを合わせる
Fn + → 行末にカーソルを合わせる
Ctrl + Fn + ← ファイルの先頭にカーソルを合わせる
Ctrl + Fn + → ファイルの末尾にカーソルを合わせる
Alt + ↑ 現在の行を上に移動
Alt + ↓ 現在の行を下に移動
Alt + Shift + ↑ 現在の行を上の行にコピー
Alt + Shift + ↓ 現在の行を下の行にコピー

コメント、折りたたみ編

折りたたみはマウスでクリックした方が分かりやすい気がする

ショートカット 説明
Ctrl + / 一行コメントイン/アウト
Shift + Alt + A 複数行コメントイン/アウト
Alt + Z 行を折り返すか折り返さないかトグル
Ctrl + K , Ctrl + 0 すべて折りたたむ
Ctrl + K, Ctrl + 1 なんかたくさん折りたたむ
Ctrl + K, Ctrl + / すべてのブロックコメントを折りたたむ
Ctrl + K, Ctrl + J 折りたたみをすべて展開する

検索編

ショートカット 説明
F12 変数や関数が定義されている部分を表示

参考

qiita.com

insider.10bace.com

Node.jsでAPIを写経したときのメモ

以前Node.jsを入れたので、何か書いてみたいと思いAPIを書きたくなりました。 prupru-prune.hatenablog.com

検索してみるととても参考になる記事が見つかりました。 qiita.com

この記事内のコードを写経してAPIやNoSQL何たるを幾何か理解できました。その過程で遭遇したいくつかのエラーとその解決方法を残しておきます。

TypeError: Cannot read properties of undefined (reading 'name')

POST、PUT、メソッドにおいてreq.body.nameを使おうとすると出てくるエラー。console.log( req.body )かres.send( req.body )するとわかりますが、実はbody自体undefinedになってます。

こちらの解決策が見つかりました。曰く、以下のbody-parserの設定をコードの頭の方でするといいらしい

app.use(Express.json());// body-parser settings

cannot use import statement outside a module node

body-parserの設定に使うExpress.json()を使うため、以下のようなimport文をindex.jsに書きました。

import Express from "express"

するとcannot use import statement outside a module node というエラーが出ました。モジュール外ではimport文は使えませんと言ってるみたいですね。

こちらの記事によれば、package.jsonのtypeという要素をmoduleにすればよいとのこと。

{
  "name": "noderest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",       /* 追加する行 */
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.3"
  }
}

ようやく動きました。

参考

写経したのはこちらのページです。

qiita.com


その他の参考サイト

qiita.com

qiita.com

qiita.com

qiita.com

mebee.info

javascriptのイベントをスクリプトから発火する

クリックイベントを発火するスクリプト

下のコードはクリックイベントリスナーを登録したのち、スクリプトからクリックイベントを発火するコードです。

<!-- index.html -->

<body>
    <button id="btn">click here</button>
    <script>

    let target = document.getElementById('btn');

    window.onload=()=>{
        let evnt = new Event("click");
        target.dispatchEvent(evnt);
    }

    elem.addEventListener('click', ()=>{
        console.log("btn clicked");
    });

    </script> 
</body>


実際にイベントを発火しているのは次の部分になります。

target.dispatchEvent( event );


イベントを作成しているのは次の部分になります。

let evnt = new Event("click");


一行にまとめると次のようになります

target.dispatchEvent( new Event('click') );

キーボードイベントを発火するスクリプト

特定のキーをスクリプトから押下したい場合は次のようなイベントを作ります

let evnt = new Event( "keydown",{key: "a" })

イベントの作成

先ほどのコードではEventコンストラクターを用いてイベントを作成しましたが、Eventを基にしたインターフェースが数多く存在し、そちらの方がイベントの情報を詳細に含んでいたりします。Eventを基にしたインターフェースを便宜上派生インターフェースと呼ぶことにしましょう。


たとえば、Event('click')と派生インターフェースMouseEvent('click')はどちらもクリックイベントを表します。

// どちらも同じような動作をする

target.dispatchEvent( new Event('click') );
target.dispatchEvent( new MouseEvent('click') );


ただし、派生インターフェースの方が多くのメンバを持っています。派生インターフェースでしか得られない情報としてはイベントの座標情報などがありますが、そうした情報が必要ではないのであればおそらくどちらを使ってもいいのだと思います。

注意点

javascriptでは独自の名前を付けたカスタムイベントを作成することが出来ます。しかし、この機能のせいでイベント名が間違っていても無視されます。例えば、clickイベントを間違ってonclickとしてイベントリスナーに登録してもエラーを吐いてくれません。

// onclickをclickと間違えても何もエラーを吐いてくれない

elem.addEventListener('onclick', (e)=>{
    console.log("btn clicked");
});


イベントリスナーの登録に限らず、イベントを生成するときもイベント名についてのエラーが吐かれません。

// clckをclickと間違えてもエラーを吐いてくれない
let evnt = new Event('clck');
let evnt = new MouseEvent('clck');

参考

developer.mozilla.org

developer.mozilla.org

ttyrecとttygifの使い方

実行環境

項目 バージョン
OS Ubuntu 20.04 LTS

インストール

$ sudo apt update
$ sudo apt install ttyrec ttygif xdotool

ttyrecをインストールするとttyplayもインストールされる

使用方法

録画、録画停止

$ export WINDOWID=$(xdotool getwindowfocus)
$ ttyrec out.tty
$ ./hoge
.
.
.
$ ctrl + D



再生

$ ttyplay out.tty



gif化

$ ttygif out.tty

参考

walialu.com 0xcc.net

fprintf()とファイル構造体とバッファリングについて

fprintf()は出力データをバッファリングするという記述をよく見かけますが、実際にバッファリングされていることを確かめたかったのでC言語で実験してみました。printf()系の関数は多岐にわたりますが、fprintf()がわかれば十分なので以下ではfprintf()に絞って話をしていきます。

バッファリングとは

fprintf()は最終的にはwriteシステムコールというOSのシステムコールを通して文字を出力します。このシステムコールの発行にはそれなりに多くの処理が必要となり、できる限り回数を抑えたいというのがシステムのお気持ちです。例えば、"abc...xyz"という26文字の文字列をファイルに出力する場合、一文字ずつ出力しようとすると計26回writeシステムコールを呼ばなければなりません。一方、"abc...xyz"を一文字ずつバッファーに蓄えておき、バッファーの先頭ポインタをwriteシステムコールに教えてあげれば、1回のシステムコールで文字列を出力することができます。このような仕組みを、出力データをバッファリングすると言ったりします。

バッファリングされたデータがフラッシュされるのは、主に以下の4つの場合*1らしいです。

  1. バッファサイズ以上のデータがバッファリングされようとしたとき
  2. 改行コードがバッファリングされたとき
  3. fflush()されたとき
  4. fcloseが実行されたり、プログラムが正常終了したとき

fprintf()は直接ファイルに出力する関数ではなく、あくまでファイルのバッファにデータを書き込む関数です。上記のいずれかの場合にOSがバッファリングされたデータを実際にファイルに書き込んでくれます。

つまり、ファイルを編集したいときはアプリケーション側ではバッファにデータを書き込めばOSが良い感じにファイルに書き込んでくれます。

同様に、ファイルを読み込みたいときはOSが良い感じにファイルのデータをバッファに取り込んでくれて、アプリケーション側ではバッファからデータを読み込むだけで良いです。

ファイル構造体の構造

次の構造体の型の定義はLinuxのファイル構造体のうち重要そうな部分を抜き出したものになります。

/* 
 *       /usr/include/x86_64-linux-gnu/bits/stdio.h 
 */

struct _IO_FILE
{
  .
  .

  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;   /* Current read pointer */
  char *_IO_read_end;   /* End of get area. */
  char *_IO_read_base;  /* Start of putback+get area. */
  char *_IO_write_base; /* Start of put area. */
  char *_IO_write_ptr;  /* Current put pointer. */
  char *_IO_write_end;  /* End of put area. */
  char *_IO_buf_base;   /* Start of reserve area. */
  char *_IO_buf_end;    /* End of reserve area. */

  .
  .
};

_IO_buf_baseや_IO_buf_endという名前からわかるように、何気なく使っていたFILE構造体にはバッファリングのためにchar型へのポインタが含まれているみたいですね。fseek()は_IO_read_baseや_IO_write_baseの値をいじっているのだという予想ができます。

標準出力への出力だとしてもファイル構造体を使っているので、バッファリングが有効になっているわけですね。

コードを書いて実験

以下のコードは"hoge"は直ちに画面に表示されますが、"fuga"はsleep(3)の後に表示されます。printf()で出力した文字列が直接画面に表示されるのではなくバッファリングされていることがよくわかります。

#include<stdio.h>
#include<unistd.h>
int main(void)
{
        printf("hoge");
        fflush(stdout);
        printf("fuga");
        sleep(3);
        fflush(stdout);
        printf("\n");
        return 0;
}

実行結果

f:id:prupru_prune:20220417003001g:plain
printf()のタイミングとフラッシュのタイミングをずらした場合

標準出力への出力のバッファリングだけではなく、通常のファイルへのデータのバッファリングもみてみましょう。

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main()
{
        FILE *fpr, *fpw;
        char line[20]={'e','m','p','t','y','\n'};
        fpw = fopen("test","w");

        // 末尾に\nがないので、出力バッファにバッファリングされるだけでフラッシュされない
        fprintf(fpw,"%s","abcde");
        fpr = fopen("test","r");
        fread(line, sizeof(char), 20, fpr);

        // "empty"が出力される
        printf("%s\n",line);

        // 強制的にバッファリングされたデータをフラッシュ
        fflush(fpw);

        // fpr->_IO_read_base を先頭に戻す
        fseek( fpr, 0, SEEK_SET );

        fread(line, sizeof(char), 20, fpr);

        // "abcde"が出力される
        printf("%s\n",line);


        fclose(fpr);
        fclose(fpw);
        return 0;
}

参考

a-hisame.hatenadiary.org

atmarkit.itmedia.co.jp

*1:参考にしたサイトとは計上の仕方を変えている