題名の通りです。OpenGLのテッセレーションシェーダについて、結構つまづいたので理解した範囲内で解説できたらなと思います。日本語の初心者向けの解説記事がとても少なくて大変でした。私の理解度が低すぎて説明するまでもないからそういったものが存在しないのか、はたまた私だけwwwとは違ったインターネットに接続しているのか、頭痛の種です。
テッセレーションシェーダ(Tessellation shader)とは何か
- シェーダの一つです。あとで詳しく書きますが、テッセレーションシェーダは二つのプログラムから成り立ちます。
- このシェーダでは端的に言えば、直線 or 三角形 or 四角形を細分化することでよりなめらかに曲面(曲線)をレンダリングできます。ゲームのマップや波面を美しくすることができます。
- 画面に近いものほど分割数を増やし、遠いものは分割の数を下げることでパフォーマンスをうまく担保することができます。また、これはシェーダ内で行われ(OpenGL側ではないという意味)動的に計算されます。(LOD:Level Of Detail という)
- OpenGL 4.0から対応しています。
シェーダパイプライン
上で書いた通り、テッセレーションシェーダは二つのシェーダから成り立ちます。一つ目がテッセレーション制御シェーダ(TCS: Tessellation Control Shader)そしてもう一つがテッセレーション評価シェーダ(TES: Tessellation Evaluation Shader)です。長いので以下、TCSとTESと書かせていただきます。
これらがどうのようにOpenGLに使われるかというと、次の図をご覧になってください。
基本的に必須なシェーダはVertex Shader(VS)とFragment Shader(FS)ですが、テッセレーションシェーダはVSのすぐ後、Geometry Shader(GS)の前の位置にきます。知っての通りテッセレーションシェーダもジオメトリーシェーダも任意です。テッセレーションシェーダはTCS→TESの順番で評価されます。(コントロールしてから評価する、といった流れ)テッセレーションを使う場合、TESは必須になりますが、TCSはなくても構いません。その代わりデフォルトのTCSの設定が使われます。
はじめに
テッセレーションシェーダを使うときに感じたことは、頂点の扱いが普通とは異なる、ということです。最初に述べた通り、テッセレーションシェーダでは、「直線 or 三角形 or 四角形」を「分割」してレンダリングします。いやいやいや、OpenGLって四角形のドローコールないやん、って思いませんか?(三角形を組み合わせるとかはなしにして)しかし、このシェーダでは四角形の概念があるのです!あとでどういう意味なのか説明したいと思います。
さて、いくつか異なる点があるんですが、特筆すべきはドローコールが少し変わることです。
// glDrawElementsなども可
glDrawArrays(GL_PATCHES,first,count);
第一引数が普通はGL_TRIANGLESやGL_LINESですが、GL_PATCHESになります。これはテッセレーションシェーダを使っていることを意味します。
「パッチ」とはプログラマが好きな数だけ頂点情報(位置や法線をも含む)を定義したもの、と説明されているのですが、はじめ私がこれを読んでもよく分かりませんでした。漠然としているので具体例を出して説明します。
説明のために、用意されたN個の頂点情報を $v_0,v_1,...v_N$ としたときに、
まず例えば、GL_TRIANGLEを使う場合を考えてみたいと思います。三角形なので頂点3つが1組としてGLSLに渡されるイメージですよね。($v_0,v_1,v_2$),($v_3,v_4,v_5$),...と3つごとに頂点の組が、一つの三角形の描画に使われます。
しかし、テッセレーションシェーダを使う場合は事情が異なってきます。一つの図形を表示するのに使う頂点の個数を定義できるのです。つまり、n個の頂点を使って一つ図形を書くように設定すると($v_0,v_1,..v_{n-1}$),($v_n,v_{n+1}...v_{2n-1}$)..と言った具合に、n個を一つの組としてシェーダに送られるイメージになります。
その設定は
glPatchParameteri(GL_PATCH_VERTICES,n); // n は int
で定義できます。一度に任意の個数の頂点情報を使うことができる、というわけです。
では無限個使えるか?というと無理なわけで、上限は次の関数で調べられます。
GLint maxtessellation;
glGetIntegerv(GL_MAX_PATCH_VERTICES,&maxtessellation);
規格では、32以上にならなければいけないということなので、少なくとも32個の頂点を同時に使って図形を書くことができることになります。(私の環境では32でした)
四角形を書く
全てを説明しきっていないのですが、ここからは実際にコードを書きつつ説明していきたいと思います。テッセレーションシェーダをいきなり理解するのは本当に難しいと思います。(そう思うのは私だけですかね?)テッセレーション特有のキーワードも出てきます。くどいかもしれませんが丁寧に追っていきますので辛抱ください。
最初に述べたように「直線 or 三角形 or 四角形」の中から分割の仕方を選択することができます。今回は四角形を選択します。
まずは普通に四角形を描いて見たいと思います。(この四角形と上の四角形は少し意味が異なります。)
typedef struct BaseVertex{
BaseVertex():BaseVertex(0,0,0){} //デフォルトがないとQVectorに追加できない
BaseVertex(float x, float y, float z){
pos[0] = x; pos[1] = y; pos[2] = z;
}
float pos[3];
} BaseVertex;
// 簡単のため正方形を試す
QVector<BaseVertex> vertex;
BaseVertex v0(-0.5,-0.5,0.0),v1( 0.5,-0.5,0.0),
v2( 0.5, 0.5,0.0),v3(-0.5, 0.5,0.0);
vertex << v0 << v1 << v2 << v3;
QVectorはQtのコンテナの一つでstd::vectorとほぼ同じです。<< 演算子がstd::vectorのpush_backに相当します。次にvaoやvboを設定します。
glGenVertexArrays(1,&vao);
glBindVertexArray(vao);
glGenBuffers(1,&vbo);
glBindBuffer(GL_ARRAY_BUFFER,vbo);
glBufferData(GL_ARRAY_BUFFER,sizeof(BaseVertex)*vertex.size(),
vertex.constData(),GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,0);
glBindVertexArray(0);
ここまでは至って普通で、次もいたって普通なVSとFSを書いてみます。
#version 400 core
layout(location=0) in vec4 VertexPosition;
void main(){
gl_Position = VertexPosition;
}
#version 400 core
layout(location=0) out vec4 FragColor;
void main(){
FragColor = vec4(1.0);
}
VSはただのパススルーシェーダで、ただ位置をそのまま流すだけです。今回は煩雑になるのを避けるためあらかじめ頂点を全て(-1~1)に納めています。仮に変換行列を使う場合としてもここで使いません。(計算しやすさのため、多分VSでは使わないほうがいいはず)TESで使うので、いずれにしてもVSはパススルーにしたほうがいいと思われます。
次にOpenGLプログラムを作成します。
QOpenGLShaderProgram *program = new QOpenGLShaderProgram;
program>addShaderFromSourceFile(QOpenGLShader::Vertex,"vertex.vs");
program>addShaderFromSourceFile(QOpenGLShader::Fragment,"fragment.fs");
program->link();
QtライブラリのQOpenGLShaderProgramというクラスを使っています。元のgl系の関数を使う場合、こちらのサイトがとても分かりやすいです。(いつもお世話になっています)GLSLプログラムの作成時には、TCSにはGL_TESS_CONTROL_SHADER、TESではGL_TESS_EVALUATION_SHADERを定数として使ってください。
glClear(GL_COLOR_BUFFER_BIT);
program->bind();
glBindVertexArray(vao);
glDrawArrays(GL_LINE_LOOP,0,vertex.size()); // 線を一周するようにレンダー
glBindVertexArray(0);
これをドローコール部分で使えば、次のように描画されるかと思います。
テッセレーションシェーダを使う
いよいよ本題です。最初に述べたとおり、まずはパッチの設定をします。今回は4つの頂点をひと組にして使いたいので
glPatchParameteri(GL_PATCH_VERTICES,4); // 一度に使う頂点を4つに設定する
これによってテッセレーションシェーダを持つGLSLプログラムがバインドされている時には、4つごとにattribute変数を使うようになります。したがって、頂点数が4の倍数になるように用意します。(余りは無視されるっぽい)
敢えてTCSとTESを先に提示して結果から先に見たいと思います。
// TCS(テッセレーション制御シェーダ)
#version 400 core
layout(vertices=4) out;
const float tessLevelOuter = 2.0; // 分割レベルを2にしておく
const float tessLevelInner = 2.0;
void main(){
// 頂点をそのまま使う
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
// 外側の分割レベルを設定する
gl_TessLevelOuter[0] = tessLevelOuter;
gl_TessLevelOuter[1] = tessLevelOuter;
gl_TessLevelOuter[2] = tessLevelOuter;
gl_TessLevelOuter[3] = tessLevelOuter;
// 内側の分割レベルを設定する
gl_TessLevelInner[0] = tessLevelInner;
gl_TessLevelInner[1] = tessLevelInner;
}
// TES(テッセレーション評価シェーダ)
#version 400 core
layout(quads) in; //「直線(isolines) or 三角形(triangles) or 四角形(quads)」のうち、
// ここでは四角形を選択している
//4つの頂点を送っているから四角形を選択している、というわけではないことに注意
void main(){
float u = gl_TessCoord.x;
float v = gl_TessCoord.y;
// そのまま頂点座標を代入しておく
vec4 v0 = gl_in[0].gl_Position;
vec4 v1 = gl_in[1].gl_Position;
vec4 v2 = gl_in[2].gl_Position;
vec4 v3 = gl_in[3].gl_Position;
gl_Position = v0*(1.0-u)*(1.0-v) + v1*u*(1.0-v)
+v2*u*v + v3*(1.0-u)*v;
}
初見だとやばいですよね。見たことないbuild-in変数多すぎます。とりあえず続けます。
// プログラムリンクする前にシェーダを追加し、その後リンクする
program->addShaderFromSourceFile(QOpenGLShader::TessellationControl,
"tessellation.tcs");
program->addShaderFromSourceFile(QOpenGLShader::TessellationEvaluation,
"tessellation.tes");
そして最後に
glDrawArrays(GL_PATHCES,0,4);// GL_LINE_LOOP を GL_PATCHESに変更
こんな感じの四角形が現れるかと思います。いやいや、どこが分割されとるんじゃー!となりますが、分割された上三角形が白塗りされてしまっているので見えていないだけです。可視化するためにGSを使って見ます。
#version 400 core
layout(triangles) in;
layout(line_strip,max_vertices=4) out;
void main(){
gl_Position = gl_in[0].gl_Position;
EmitVertex();
gl_Position = gl_in[1].gl_Position;
EmitVertex();
gl_Position = gl_in[2].gl_Position;
EmitVertex();
gl_Position = gl_in[0].gl_Position;
EmitVertex();
EndPrimitive();
}
このジオメトリシェーダでは、TESから送られてきた分割された断片の三角形を一周する線に修正しています。次のようになっていることが確認できると思います。テッセレーションシェーダと同様にして、GLSLプログラムに追加します。gl系の定数はGL_GEOMETRY_SHADERとなっています。GSで行なっていることをざっと説明するとこんな感じです。
テッセレーション評価シェーダからは三角形のprimitiveを受け取るので、入力をlayout(triangles) in;
を指定。その三角形をフレームにしたいので出力をlayou(line_strip,max_vertices=4) out;
にして頂点を0→1→2→0の順番で結びprimitiveを三角形から線分の繋がりへと修正します。テッセレーションシェーダを使う場合、ジオメトリーシェーダで受け取ることのできるprimitiveは線と三角形のみになります。GSがメインではないのでこれぐらいにします。
レンダーすると...
おお!ちゃんと分割されていますね!やったね!実験しながらTCSを理解する
まずはTCSを眺めて見ます。最初のlayout修飾子でverticesを4に設定しています。これは次のステージTESに渡す頂点の数を設定しています。glPatchParameteriで設定した頂点組みの数と必ずしも一致させる必要はありません(ただ実際は同じ数を設定しておくのが妥当)
また、この送り出す頂点の回数分TCSは呼び出されるようです。(そうだったはず)
次にgl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
に触れて見たいと思います。gl_out/in構造体は次のステージに送られる頂点情報です。GSでも出てきましたが、次のような構造体になっています。
in gl_PerVertex
{
vec4 gl_Position; // 頂点
float gl_PointSize; // 点の大きさ(GL_POINTSでレンダーした時のみ有効
float gl_ClipDistance[]; // 割愛
} gl_in[gl_MaxPatchVertices];
gl_inは前のシェーダから受け取る頂点情報で、この二つは中身は変わりません。配列になっているのは頂点が複数同時に前のステージから送られてくるためです。今回は4つの頂点を一組にしているので大きさが4の配列になっています。上のステートメントでは、TESに送るgl_InvocationID番目の頂点に、VSから送られてきたgl_Invocation番目の頂点を代入しています。つまり、そのまま渡している、ということです。gl_Invocationというbuild-in変数なんですが、これは頂点のインデックスです。つまり、v0を処理するときgl_InvocationIDは0になり、v1を処理するときgl_InvocationIDは1です。なので次のように書き直しても構いません。
// これをやめて
// gl_out[gl_InvocationID].gl_Position=gl_in[gl_InvocationID].gl_Position;
// gl_InvocationIDが具体的にわかるような書き方をする
if(gl_InvocationID == 0){
gl_out[0].gl_Position = gl_in[0].gl_Position;
}else if(gl_InvocationID == 1){
gl_out[1].gl_Position = gl_in[1].gl_Position;
}else if(gl_InvocationID == 2){
gl_out[2].gl_Position = gl_in[2].gl_Position;
}else if(gl_InvocationID == 3){
gl_out[3].gl_Position = gl_in[3].gl_Position;
}
先ほど述べたように、TESには4つの頂点を出力するようにしています。敢えてlayout(vertices=5) out;
にして見たらどうでしょう?gl_Invocationは0~4になります。なぜなら5つ頂点を送り出すからです!しかし、送られてきた頂点は4つです。なのでgl_out[]のサイズは5、gl_in[]のサイズは4という具合になるのではないか、と推測します。(あくまで推測ですが。)パッチの頂点数とTCSからTESに送り出す頂点数(vertices)は同じ方がいい、というのは頂点情報をそのまま渡すのが自然(かつ楽)だからということです。
次に、gl_TessLevelOuter(/Inner)をいじってみます。
先のtcsのコードではtessLevelOuterというグローバル変数を代入しています。これを変化させてみると次のようになります。(tessLevelInnerは2のまま)
見えずらくて申し訳ないですが、違いがわかりますでしょうか?各辺の分割数が違います。今度は配列によって違う値を与えてみます。
もう分かりましたね?gl_TessLevelOuterは各辺の分割数を設定します。それゆえのtessellation Control(制御) shaderなのです。ここで鋭い人は気づくかもしれませんが、gl_TessLevelOuter[]のサイズは4です。さらに言えば、今回は四角形で分割するようにしていて、その四角形は辺を4つ持ちます。なのでgl_TessLevelOuterの0から3まで値を設定しています。では三角形はどうでしょう?辺は3つです。したがって、gl_TessLevelOuter[3]は三角形では使われません!同様に直線で使われるのはgl_TessLevelOuter[0]だけです!だんだん分かってきましたね。
一応gl_TessLevelInnerについても触れます。tessLevelInnerを変化させるとこんな感じ。(tessLevelOuterは6に設定)
正直法則が分かりにくいですね。中の図形が増えているのが確認できます。Innerについてはあんまり考えなくてよさそうです。gl_TessLevelInner[]のサイズは2です。言えることは、OuterもInnerも同じ数に設定しておくと綺麗に見えるでしょう。(一番右の写真でわかる通り)
まとめると、TCSではVSから送られてきた頂点データを使って、verticesで定められた数の頂点をTESに出力します。また、分割の仕方も設定している、となっています。
ちょっと余談
最初にテッセレーションシェーダを使うとき、TESは必須だが、TCSは任意だと書きました。振り返ってみると、TCSで頂点データをそのまま同じ個数TESを渡すのは別にOpenGLが勝手にやってくれてもいいような気がしますし、細分化のレベルを決めるのもわざわざ別ファイルにシェーダを書かなくても設定できそうな気がしませんか?というわけでTCSを使わない場合については次のように設定できます。
GLfloat tessLevelOuter[4] = {1.0f,3.0f,5.0f,9.0f};
glPatchParameterfv(GL_PATCH_DEFAULT_OUTER_LEVEL,tessLevelOuter);
GLfloat tessLevelInner[2] = {2.0f,2.0f};
glPatchParameterfv(GL_PATCH_DEFAULT_INNER_LEVEL,tessLevelInner);
このようにOuter/Innerのデフォルト値を設定することができます。頂点情報についてはそのまま渡されます。TCSを書かない場合、これらの値が使われます。それとfloatを使っていますが、これは全て切り上げされた整数としてgl_TessLevelOuter(/Inner)に格納されます。なぜfloatで代入されるのかといえば、LODのためでしょうか。
TESを見てみる
TESについて説明したいと思います。
まず今回のプログラムがどのように動いているのかを見て、その後一般化したことを議論したいと思います。
layout(quads) in;
最初にlayout修飾子でquadsを指定しています。これは、テッセレーション を四角形の座標系で評価するということを指定しています。(何を言っているんだ...?)他の選択肢はisolines
,triangles
があります。疑問が残ると思いますが、ちょっと先に進んで見ます。
float u = gl_TessCoord.x;
float v = gl_TessCoord.y;
この四角形の座標系はテクスチャ座標(u-v)と同じになっています。gl_TessCoordのxy座標がuvに対応しています。わざわざgl_TessCoordと書くのはめんどくさいので新しく変数を用意しています。gl_TessCoord.zwに関してはquadsを使用した場合、使われません。
// TCSから送られてきたデータをそのまま代入している
vec4 v0 = gl_in[0].gl_Position;
vec4 v1 = gl_in[1].gl_Position;
vec4 v2 = gl_in[2].gl_Position;
vec4 v3 = gl_in[3].gl_Position;
gl_Position = v0*(1.0-u)*(1.0-v) + v1*u*(1.0-v)
+v2*u*v + v3*(1.0-u)*v;
$v_0,v_1,v_2,v_3$はTCSから送られてきた頂点座標を表しています。gl_in[]のサイズはここまで理解していれば分かってると思います。TCSで指定したverticesの大きさと同じです。
さて、少し数学的なお話を考えて見たいと思います。
上の図のV0,V1,V2,V3は$v_0,v_1,v_2,v_3$の座標に対応しています。これらを用いてPを表すとするとどうなるでしょうか?
じっくりみると、uが大きいとき、Pへの寄与が大きいのは$v_1,v_2$です。なので$P\propto v1 * u$、$P\propto v2 * u$の関係があると推測できます。逆に$v_0,v_3$に関しては逆比例で$P\propto v0 * (1 -u)$,$P\propto v3 * (1 - u)$ となります。同様にv方向についても考えれば
// 図中のPがgl_Positionに対応
gl_Position = v0*(1.0-u)*(1.0-v) + v1*u*(1.0-v)
+v2*u*v + v3*(1.0-u)*v;
というようにPを表すことができます。具体的にu,vに代入して頂ければわかると思います。OpenGL側で送ったデータがようやく最終地点へ到達しました。FSでは頂点の位置を変更しないので頂点データのゴールはTESです。(いやー長い)
さて、この(u,v)の値は次のようになります。
(u,v)はTCSで設定した情報に基づいて離散的に変化します。gl_TessLevelOuter(/Inner)は全て2に設定してあります。よって各辺が二つに分割されるべきなので、uとvが0.5という値を取っています。逆に言えば、各辺を二つに分けるためなので、u,vは0.0,0.5,1.0の値しかとりません。
左下の三角形(黄線でハイライトされた部分)は(u,v)の組みが、(0.0,0.0),(0.5,0.0),(0.5,0.5)のときの 頂点Pで構成され、次のシェーダに送られている、というわけです。
実験しながらTESを理解する
私は最初、quads指定子の意味が分かりませんでした。というのも私が持っている本には、16個の頂点がTESに送られてくるサンプルがあり、16-4=12個の頂点は余分(なんのためにあるんだ??)に思えてしまったからです。頂点4つの四角形を分割するのに、なぜ頂点を16個使うのか ...
1日考えた結果、そもそもこの考え自体全くが正しくないと気づいたのです!
逆だ!逆!
頂点があるから(u,v)が決められるのではない。(u,v)があるから頂点が決まるのだ!、と。
具体的に言えば
(0.0,0.0)のu,vを使って計算したときのgl_Positionを左下の頂点にする。
(0.5,0.0)のu,vを使って計算したときのgl_Positionを左中央の頂点にする。
(0.5,0.5)のu,vを使って計算したときのgl_Positionを中央の頂点にする。
...
といった具合に、(u,v)が重要なのであって、送られてくる頂点自体は最終的な頂点を決定するための材料にすぎないのだということです。何度か述べていますが、quads指定子の四角形は、パッチの頂点の個数となんら関係がありません。quads指定子は(u,v)座標を使って、四角形のイメージで分割するように指定します。パッチとは、四角形を構成する頂点を計算するのに必要な任意の数の頂点の集合だと言えます。TCSで分割数を変更すればそれに伴って(u,v)のとる値も変わります。
これまでのまとめを含め、少し実験的します。
まずパッチの個数を4ではなく5にします。頂点の数も一つ増やします。
BaseVertex v4(0.2,0.3,0.0);
vertex << v4;
glPatchParameteri(GL_PATCH_VERTICES,5); // 4から5に変更
layout(vertices = 5) out;
// 上は省略
// 5つ目を追加し、v0に代入してみる
vec4 v4 = gl_in[4].gl_Position;
v0 = v4;
// v0 が v4 になっている
gl_Position = v0*(1.0-u)*(1.0-v) + v1*u*(1.0-v)
+v2*u*v + v3*(1.0-u)*v;
注目して欲しいことは3つ。
①左下方向に捻じ曲がっていること。
②5つ頂点をパッチで渡しているが、結果は四角形。
③5つの頂点が送られてくるので、TESではgl_in[4]を使うことができる。
特に②番目の事実がテッセレーションシェーダを理解するのに一番大事じゃないんでしょうか。つまり、送られてきた頂点データは、先ほども述べた通り、分割された頂点位置を計算することのみに使われているだけです。パッチからのデータの個数は全く関係ありません。今回右上の頂点をV0 = V4としたことで、V0の値が小さくなってしまいました。したがって全体的に左下に歪んだ形になっています。
抽象的なことを言えば、gl_in[]は元のデータであり、それらと一緒にu,v座標を重み付けのパラメータとして使って、新たに頂点位置を計算します。どのデータが新しい頂点にどのぐらい寄与するのかuv座標を使ってを計算しているわけです。
このgl_Positionに与えられる値は最終的な頂点位置になり、よってここで変換行列を入れるべきです。
四角形以外の座標系(直線と三角形)
TESのlayout修飾子でtrignalesを指定すると、次のような座標系(gl_TessCoord)の値で頂点が計算されます。
TESのソースコードは次のようです。
#version 400 core
layout(triangles) in;
void main(){
float u = gl_TessCoord.x;
float v = gl_TessCoord.y;
float w = gl_TessCoord.z;
vec4 v0 = gl_in[0].gl_Position;
vec4 v1 = gl_in[1].gl_Position;
vec4 v2 = gl_in[2].gl_Position;
gl_Position = v0 * u + v1 * v + v2 * w;
}
四角形からアナロジーでわかるかと思います。
少し変わった座標系で、重心座標系というようです。復習ですが、TCSのgl_TessLevelOuter[3]は影響しません。その代わり四角形とは違い、重み付けのパラメータがuvに加え、wが追加されました。
直線に関してはlayout修飾しでisolinesを指定します。この場合、gl_TessCoord.xがuとして使われ1次元になります。そのまま渡せばただの直線になり面白みがないので今回は省略します。
最後に
お疲れ様でした。私の説明が至らない部分も多いですが、読んでくれた方が少しでも理解が捗ってくれればと思う次第です。
どうでもいい話になりますが、自分の使ってるpcは MacBook Pro (13-inch, 2017)で2017年の最後ぐらいに買いました。まぁ割と新し目な訳です。しかしOpenGLのドライバが4.1までしか対応していないんです!OpenGL4.1って2010年に発表されてるようで、「おいおい、そんな古いのにしか対応していないんか」と後になって気づきました。Computer shader や Shader Storage Buffer Object とかも試してみたいのに、ハードウェア的にできず、とても悲しいです。しかも最近、AppleはOpenGLをmacOS10.14から非推奨に切り替える方針を発表しました。なので私はいまだにHigh Sierraのまま。怖くてアップデートができません。はぁ。(windowsのノーパソ欲しいな)
それともう一つ。
このノートpcの解像度が2560×1600でして、なんちゃって4K画質(WQHD?)になっています。それがどうやら「High DPI Displays」というものに該当しまして2ピクセルのレンダリングが1ピクセルに圧縮されている(?)ようです。なのでglViewport()で縦横を指定するときに、ウィンドウサイズを2倍にした値を入れてあげないといけません。(チーン)QtCreatorでもXcodeでも試しましたが変更できないっぽいですね。これって2*2倍計算しているんでしょうか?
ちなみにゲームのFortniteをプレイすると、推奨スペックを満たしているのにも関わらず、落ちます。(チーン)
気が向けば、応用編的なものも書きたいと思います、はい。
参考にしたサイトなど
NVIDIAのpdf
[https://www.nvidia.com/content/gtc-2010/pdfs/2227_gtc2010.pdf]
(https://www.nvidia.com/content/gtc-2010/pdfs/2227_gtc2010.pdf)
OpenGLのwiki
https://www.khronos.org/opengl/wiki/Tessellation
書籍
OpenGL-シェーディング言語-実例で覚えるGLSLプログラミング-