1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
<!DOCTYPE html><html lang="ja"><head>
<meta charset="utf-8">
<title>のレスポンシブデザイン</title>
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@threejs">
<meta name="twitter:title" content="Three.js – のレスポンシブデザイン">
<meta property="og:image" content="https://threejs.org/files/share.png">
<link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
<link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="../resources/lesson.css">
<link rel="stylesheet" href="../resources/lang.css">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "../../build/three.module.js"
}
}
</script>
</head>
<body>
<div class="container">
<div class="lesson-title">
<h1>のレスポンシブデザイン</h1>
</div>
<div class="lesson">
<div class="lesson-main">
<p>これはthree.jsの2番目の連載記事です。
最初の記事は <a href="fundamentals.html">Three.jsの基礎知識</a> でした。
まだ読んでいない場合はそこから始めて下さい。</p>
<p>この記事はthree.jsアプリをどんな状況にもレスポンシブにする方法を説明します。
一般的なレスポンシブ対応のWebページはデスクトップやタブレット、スマートフォンなど異なったディスプレイサイズに対応します。</p>
<p>three.jsの場合、さらに考慮すべき状況があります。例えば3Dエディターで左・右・上・下に何かを制御したい場合です。このドキュメントの真ん中にあるコードが一つの例です。
最後のサンプルコードはCSSでサイズ指定なしのcanvasを使ってます。</p>
<pre class="prettyprint showlinemods notranslate lang-html" translate="no"><canvas id="c"></canvas>
</pre>
<p>このcanvasのデフォルトサイズは300 x 150です。
Web上ではCSSでサイズ指定する事が推奨されています。
CSSを追加しcanvasをWebページ一杯にしましょう。</p>
<pre class="prettyprint showlinemods notranslate lang-html" translate="no"><style>
html, body {
margin: 0;
height: 100%;
}
#c {
width: 100%;
height: 100%;
display: block;
}
</style>
</pre>
<p>bodyのmarginはデフォルトで5ピクセルのためマージンを0にします。
htmlとbodyの高さは100%にしウィンドウ一杯に設定します。
そうしないとhtmlとbodyはbody内のコンテンツと同じぐらいのサイズにしかなりません。</p>
<p>次にbodyのコンテナーである <code class="notranslate" translate="no">id=c</code> のelementが100%のサイズになるようにします。</p>
<p>最後にそのコンテナーの <code class="notranslate" translate="no">display</code> を <code class="notranslate" translate="no">block</code> に設定します。canvasのdisplayのデフォルトは <code class="notranslate" translate="no">inline</code> です。インライン要素は表示されているものに空白を追加してしまう事があります。このような場合はcanvasを <code class="notranslate" translate="no">block</code> に設定するとこの問題は解消されます。</p>
<p>その結果がこちらにあります。</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive-no-resize.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/responsive-no-resize.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
</div>
<p></p>
<p>canvasがページを埋め尽くすようになりましたが、2つ問題があります。
1つはキューブが伸びています。キューブは立方体でなく箱のようなものです。高すぎて広がりすぎています。サンプルを開いてブラウザのウィンドウサイズをリサイズすると、キューブが伸びていて高すぎるのがわかります。</p>
<p><img src="../resources/images/resize-incorrect-aspect.png" width="407" class="threejs_center nobg"></p>
<p>2つ目の問題は解像度が低い、または濃淡にムラがありぼやけて見える事です。ウィンドウを大きく引き伸ばすとこの問題がわかります。</p>
<p><img src="../resources/images/resize-low-res.png" class="threejs_center nobg"></p>
<p>まず引き伸びている問題を解決しましょう。そのためにはカメラのアスペクトをcanvasの表示サイズのアスペクトに設定する必要があります。canvasの <code class="notranslate" translate="no">clientWidth</code> と <code class="notranslate" translate="no">clientHeight</code> を参照する事で設定を行う事ができます。</p>
<p>レンダーのループ処理を次のように更新します。</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
time *= 0.001;
+ const canvas = renderer.domElement;
+ camera.aspect = canvas.clientWidth / canvas.clientHeight;
+ camera.updateProjectionMatrix();
...
</pre>
<p>これでキューブが歪むのを止められます。</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive-update-camera.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/responsive-update-camera.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
</div>
<p></p>
<p>サンプルを別ウィンドウで開きウィンドウのサイズを変更すると、キューブが縦にも横にも伸びていない事がわかるはずです。ウィンドウの大きさに関係なく、正しいアスペクトを保っています。</p>
<p><img src="../resources/images/resize-correct-aspect.png" width="407" class="threejs_center nobg"></p>
<p>次はブロックノイズを修正していきましょう。</p>
<p>キャンバス要素には2つのサイズがあります。1つ目のサイズは、キャンバスがページに表示されるサイズです。それはCSSで設定しています。2つ目のサイズはキャンバス自体のピクセル数です。これは画像と何ら変わりありません。
例えば、128 x 64ピクセルの画像を持っていて、CSSを使って400 x 200ピクセルで表示する事ができるかもしれません。</p>
<pre class="prettyprint showlinemods notranslate lang-html" translate="no"><img src="some128x64image.jpg" style="width:400px; height:200px">
</pre>
<p>キャンバス内部のサイズ、その解像度は描画バッファサイズと呼ばれます。
three.jsでは <code class="notranslate" translate="no">renderer.setSize</code> を呼び出す事でキャンバスの描画バッファサイズを設定する事ができます。
どのサイズを選ぶべきでしょうか?一番わかりやすい答えは"キャンバスが表示されているサイズと同じ"です。
もう一度キャンバスの <code class="notranslate" translate="no">clientWidth</code> と <code class="notranslate" translate="no">clientHeight</code> を見てみましょう。</p>
<p>レンダラーのキャンバスが表示されているサイズになっていないかどうかを確認し、表示されている場合はサイズを設定する関数を書いてみましょう。</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
</pre>
<p>キャンバスのサイズを変更する必要があるかどうかをチェックしています。キャンバスのサイズを変更する事は、キャンバスの仕様の興味深い部分であり、すでに必要なサイズになっている場合は同じサイズを設定しない方が良いでしょう。</p>
<p>サイズを変更する必要があるかどうかわかったら、次に <code class="notranslate" translate="no">renderer.setSize</code> を呼び出して新しい幅と高さを渡します。最後に <code class="notranslate" translate="no">false</code> を渡す事が重要です。</p>
<p>デフォルトでは <code class="notranslate" translate="no">render.setSize</code> はキャンバスのCSSサイズを設定しますが、これは私たちが望んでいるものではありません。ブラウザは他の全ての要素に対して、CSSを使用して要素の表示サイズを決定するという方法で動作し続けてほしいのです。3つの要素で使用されるキャンバスが他の要素と異なるのは避けたいのです。</p>
<p>この関数はキャンバスのサイズが変更された場合、trueを返す事に注意して下さい。この関数を使って他にも更新すべき事があるかどうかをチェックする事ができます。この関数を使ってレンダーのループ処理を修正してみましょう。</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
time *= 0.001;
+ if (resizeRendererToDisplaySize(renderer)) {
+ const canvas = renderer.domElement;
+ camera.aspect = canvas.clientWidth / canvas.clientHeight;
+ camera.updateProjectionMatrix();
+ }
...
</pre>
<p>キャンバスの表示サイズが変更されて <code class="notranslate" translate="no">resizeRendererToDisplaySize</code> が <code class="notranslate" translate="no">true</code> を返した場合のみ、カメラのアスペクトを設定します。</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/responsive.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
</div>
<p></p>
<p>これでキャンバスの表示サイズに合った解像度でレンダリングされるようになりました。</p>
<p>CSSにリサイズ処理を任せた場合のポイントを明確にするために、このコードを <a href="../examples/threejs-responsive.js">分離した <code class="notranslate" translate="no">.js</code> ファイル</a> に入れてみましょう。
ここではCSSがサイズを選択するいくつかのサンプルがあります。
それらが動作するようにゼロからコードを変更しなければならなかった事に気づくでしょう。</p>
<p>文章の段落の真ん中にキューブを置いてみましょう。</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive-paragraph.html&startPane=html"></iframe></div>
<a class="threejs_center" href="/manual/examples/responsive-paragraph.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
</div>
<p></p>
<p>エディタスタイルのレイアウトで右側のコントロールエリアのサイズを変更できるようにしたのと同じコードです。</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive-editor.html&startPane=html"></iframe></div>
<a class="threejs_center" href="/manual/examples/responsive-editor.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
</div>
<p></p>
<p>注目すべき重要な部分はコードが変更されていない事です。HTMLとCSSだけが変更されました。</p>
<h2 id="hd-dpi-">HD-DPIディスプレイの取り扱い</h2>
<p>HD-DPIとは高解像度ディスプレイの略です。
最近ではほとんどのスマートフォンと同じくらい、Macや多くのWindowsマシンで採用されています。</p>
<p>ブラウザでの動作方法は、CSSを使用してディスプレイの高解像度に関係なく同じサイズを設定する事です。ブラウザはテキストをさらに詳細にレンダリングしますが、物理的なサイズは同じです。</p>
<p>three.jsでHD-DPIを扱う方法は色々あります。</p>
<p>1つ目の方法は特に何もしない事です。これは間違いなく最も一般的な方法です。3DグラフィックのレンダリングにはたくさんのGPUの処理パワーが必要です。モバイルのGPUは少なくとも2018年時点ではデスクトップよりも電力が少ないが、それでも携帯電話は非常に高解像度のディスプレイを搭載している事が多いです。現在の上位機種はHD-DPI比が3倍という事は、非HD-DPIディスプレイの1ピクセルごとに9ピクセルを持っている事を意味します。つまり、9倍のレンダリングをしなければならないという事です。</p>
<p>9倍のピクセルを計算するのは大変な作業なので、コードをそのままにしておくと1倍のピクセルを計算して、ブラウザは3倍のサイズ(3x x 3x = 9xピクセル)で描画します。</p>
<p>重いthree.jsアプリの場合はこれが必要でしょう。そうしないとフレームレートが遅くなる可能性があります。</p>
<p>デバイスの解像度でレンダリングしたい場合、three.jsにはいくつかのデバイスを変更する方法があります。</p>
<p>1つは <code class="notranslate" translate="no">renderer.setPixelRatio</code> でthree.jsに解像度の乗数を伝える事です。
CSSピクセルからデバイスピクセルへの乗数をブラウザに伝え、それをthree.jsに渡します。</p>
<pre class="prettyprint showlinemods notranslate notranslate" translate="no"> renderer.setPixelRatio(window.devicePixelRatio);
</pre><p><code class="notranslate" translate="no">renderer.setSize</code> を呼び出し後、要求されたサイズに渡されたピクセル比を乗算したものが使用されます。<strong>これは強く非推奨です</strong>。以下を参照して下さい。</p>
<p>もう1つの方法は、キャンバスのサイズを変更する時に自分で設定する事です。</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no"> function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const pixelRatio = window.devicePixelRatio;
const width = canvas.clientWidth * pixelRatio | 0;
const height = canvas.clientHeight * pixelRatio | 0;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
</pre>
<p>この2つ目の方法の方が客観的には優れています。なぜかと言うと私が求めるものを手に入れる事ができるからです。</p>
<p>three.jsを使っていると実際のキャンバスの描画バッファのサイズを指定します。例えば、後処理フィルタを作成する場合などです。
または <code class="notranslate" translate="no">gl_FragCoord</code> にアクセスするシェーダを作成している場合、あるいは2Dキャンバスに描画するためのスクリーンショット、またはGPUピッキング用のピクセルを読み込んだ場合などに使用する事ができます。</p>
<p><code class="notranslate" translate="no">setPixelRatio</code> を使うと要求したサイズよりも実際のサイズが違ってしまう事が多々あります。いつ要求したサイズが使えるか、いつThree.jsの実際のサイズが使えるか推測しなければなりません。
これを自分で行う事で使用されているサイズが要求したサイズである事を常に知る事ができます。
裏で魔法がかかっているという特殊ケースではありません。</p>
<p>上のコードを使った例です。</p>
<p></p><div translate="no" class="threejs_example_container notranslate">
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/responsive-hd-dpi.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/responsive-hd-dpi.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
</div>
<p></p>
<p>違いがわかりにくいかもしれませんが、HD-DPIディスプレイをお持ちの方はこのサンプルを上のサンプルと比較してみて下さい。エッジがより鮮明になっている事がわかると思います。</p>
<p>基礎な内容ですがこの記事ではとても基本的な所を取り上げました。次は<a href="primitives.html">three.jsが提供する基本的なプリミティブについて簡単に説明します。</a></p>
</div>
</div>
</div>
<script src="../resources/prettify.js"></script>
<script src="../resources/lesson.js"></script>
</body></html>