#はじめに
リコーのYuuki_Sです。
弊社ではRICOH THETAという全周囲360度撮れるカメラを出しています。
RICOH THETA VやTHETA Z1は、OSにAndroidを採用しており、Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます。(詳細は本記事の末尾を参照)。
イメージベースドライティング(IBL : Image-based lighting)という技術をご存知でしょうか?
現実世界の全方向の光情報を環境情報として、CGのレンダリングに利用する手法のことです。
周辺の光源の位置や色合いをCGに反映することで、現実に溶け込んだ表現が可能になるため映像作品やゲームで使用されています。
↑IBL有無の比較(Blenderにて。背景画像はHDRI Havenより)
この手法には、前述の通り全方位の光情報、すなわち全天球画像を用います。
そして、全天球画像を取得できる手軽なデバイスとしてTHETAが存在します。
つまりIBLは、THETAととても相性が良い技術です。
実際、初代THETA発売時にIBLを試して頂いてるブログ記事 やIBL用の全天球画像作成フローを紹介したCG専門雑誌の記事も存在します。
こんなTHETAと相性のいいIBLですが、今回はこれをTHETA1台でプレビューできる環境を作ってみました。
THETAとスマホがあれば、以下の様にどこでもIBLしたCGを見ることができます。
◤Qiita記事公開◢
— THETAプラグイン開発者コミュニティ (@thetaplugin) August 7, 2020
カメラ1台でお外でImage Based Lighting。THETAプラグインでWebGLを用いIBLする方法を紹介します。https://t.co/4WVTmvB5qb#THETA #thetaplugin #CG #WebGL #threejs pic.twitter.com/vj0rTykwg7
#仕組み紹介
THETA1台とスマホでIBLをプレビューするためには、THETAの画像を利用してCGのレンダリングをおこなう必要があります。
一つの方法としては、THETAのAPIを利用して画像取得しレンダリングするスマホアプリを作る方法があります。しかし、冒頭で紹介した通り、THETA自体がAndroidを搭載しているため、THETA内で動くプラグインを作ることでよりシンプルに実現できます。
上図の通り、THETAプラグインで画像取得をおこないつつWEBサーバーも立ち上げます。このサーバー上にWebGLを用いたレンダリング環境を配置し、スマホ側からアクセスすることでブラウザ上にIBLしたCGを表示します。もちろん、パソコンからでも見ることができます。
※なお、今回はJPEG画像を利用しますが、IBLを厳密におこなう際にはHDR画像を用いる事が一般的です。ここで言うHDRとはHDR撮影したJPEG画像ではなく画像の階調を明暗含め広く保った画像を指します。通常のJPEG等の形式が1色8bitに対して、こういったHDR画像は1色16bitで情報が保存されており、より表現力の高いCG表現が可能です。
#ライブプレビューを利用したIBL
それでは、実際にライブプレビューを環境情報として利用するプラグインを書いていきます。
プラグイン開発のベースとなるプロジェクトファイル一式は、@KA-2さんの以下記事で解説したものを使用します。
THETAプラグインでライブプリビューを扱いやすくする
まずは、上記のプロジェクトにWebGLを扱うために必要な諸々を追加していきます。
最初に、WebGLを簡単に扱える便利なライブラリであるthree.jsをhttps://threejs.org/のダウンロードより入手します。
圧縮ファイルを解凍したフォルダより、"build"内のthree.min.jsを"assets"の"js"フォルダにコピーします。
次に、"assets"フォルダ内に"three"というフォルダを新規作成し、解凍したフォルダ内のsrcフォルダ、buildフォルダをコピーします。
更に、"three"フォルダ内に"examples"という名前の新規フォルダを作成し、その中に解凍したフォルダの"examples"内の"jsm"フォルダをコピーします。
最後に、”assets"フォルダ内に"models"という名前のフォルダを新規作成し、解凍したフォルダの"examples\models\gltf"にある”DamagedHelmet”フォルダをコピーします。ここまで終えると、"assets"フォルダ内は以下のような構成になります。
これで下準備は終わったので、処理を書いていきます。WebGLを扱うためには、Androidアプリ内にサーバーを立てる必要がありますが、実はtheta-plugin-extendedpreviewには既にその機能が備わっています。なので、既に存在するindex.htmlを編集するだけでWebGLによる表示が可能になります。
編集箇所ですが、要所を記載します。(コード全文は本章最後に折りたたんで記載します。)
まず、冒頭部分にタイトルと読み込むJavaScriptを記載します。
<head>
<title>WebGL Image Based Lighting</title>
<meta charset="utf-8">
<script src="js/preview.js"></script>
<script src="js/three.min.js"></script>
</head>
次にbody内でWebGLを表示する画面要素を追加します。※three.jsのサンプルコードを参考にしました。
<body onLoad="startLivePreview();updatePreviwFrame();">
<div id="container"> </div>
~
</body>
そして諸々のインポートをおこないます。
<script type="module">
import * as THREE from './three/build/three.module.js';
import { OrbitControls } from './three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from './three/examples/jsm/loaders/GLTFLoader.js';
import { RGBELoader } from './three/examples/jsm/loaders/RGBELoader.js';
import { RoughnessMipmapper } from './three/examples/jsm/utils/RoughnessMipmapper.js';
次に初期化する関数内でプレビュー画像をシーンの背景に設定します。
既にプレビュー画像はlvimgというimg要素に入るようになってますのでそれを参照します。
scene = new THREE.Scene();
image = document.getElementById('lvimg');
texture = textureLoader.load(image.src);
texture.mapping = THREE.UVMapping;
var roughnessMipmapper = new RoughnessMipmapper( renderer );
scene.background = new THREE.WebGLCubeRenderTarget( 1920, options ).fromEquirectangularTexture( renderer, texture );
scene.environment = new THREE.WebGLCubeRenderTarget( 1920, options ).fromEquirectangularTexture( renderer, texture );
そしてシーンにglTFファイルを読み込みます。
var loader = new GLTFLoader().setPath('models/DamagedHelmet/glTF/');
loader.load( 'DamagedHelmet.gltf', function ( gltf ) {
gltf.scene.traverse( function ( child ) {} );
scene.add( gltf.scene );
roughnessMipmapper.dispose();
render();
} );
最後に毎フレームで背景を更新するようにします。
function render()
{
textureLoader.load(image.src, function ( texture ) {
scene.background.fromEquirectangularTexture(renderer, texture);
scene.environment.fromEquirectangularTexture(renderer, texture);
});
renderer.render( scene, camera );
}
これでindex.htmlの編集は終了です。
ですが、このままだとWebサーバー側からJavaScriptファイルを渡す際にmimeTypeが設定されないので、そこを修正します。
WebServer.javaの以下の関数に拡張子”.js”ならmimeTypeを”text/javascript”と設定するようにします。
private Response serveAssetFiles(String uri)
{
if (uri.equals("/")){
uri = "/index.html";
}
ContentResolver contentResolver = context.getContentResolver();
String mimeType = contentResolver.getType(Uri.parse(uri));
if(mimeType == null)//追加
{
String tmp = uri.substring(uri.lastIndexOf("."));
if(tmp.equals(".js"))
{
mimeType = "text/javascript";
}
}
~略
}
ここまで済ませたら、アプリをTHETAにビルド&インストールし、スマホかパソコンからTHETAのWifiに接続、http://127.0.0.1:8888にアクセスします。
すると、以下の通り、背景にライブプレビューが設定されたCGが表示されます。
ドラッグしてカメラを動かすと写り込んでいる映像も変わります。
これにて完成!、と言いたいところですが背景がなんだかボケて見えます。
これは純粋にライブプレビューの解像度が低いためです。このプレビューでは1024x512を用いています。これより解像度の高い設定として1920x960も選択できますが、その場合フレームレートが8fpsになってしまいます。また、そもそもIBLの背景にするには1920x960でも心もとないです。
(一例としてIBL用のHDRI画像をフリーで公開しているHDRI Havenでは最大16K(16384x8192)で提供されています。)
なので、ライブプレビューは確認用として、高解像度な撮影画像も背景に設定できるように機能追加しましょう。
その前に、ここまでのコードを全文を以下に置いておきます。
ライブプレビュー表示のindex.html全文
<html>
<head>
<title>WebGL Image Based Lighting</title>
<meta charset="utf-8">
<script src="js/preview.js"></script>
<script src="js/three.min.js"></script>
</head>
<body onLoad="startLivePreview();updatePreviwFrame();">
<div id="container"> </div>
<img hidden id="lvimg" src="" width="640" height="320">
<br>
<br>
<br>
<input name="preview" type="radio" value="off" onclick="stopLivePreview();"> OFF
<input name="preview" type="radio" value="on" onclick="startLivePreview();"> ON
<br>
<br>
<input name="ev" type="radio" value=-2.0 onclick="setEv(value);"> -2.0
<input name="ev" type="radio" value=-1.7 onclick="setEv(value);"> -1.7
<input name="ev" type="radio" value=-1.3 onclick="setEv(value);"> -1.3
<input name="ev" type="radio" value=-1.0 onclick="setEv(value);"> -1.0
<input name="ev" type="radio" value=-0.7 onclick="setEv(value);"> -0.7
<input name="ev" type="radio" value=-0.3 onclick="setEv(value);"> -0.3
<input name="ev" type="radio" value=0.0 onclick="setEv(value);"> 0.0
<input name="ev" type="radio" value=0.3 onclick="setEv(value);"> 0.3
<input name="ev" type="radio" value=0.7 onclick="setEv(value);"> 0.7
<input name="ev" type="radio" value=1.0 onclick="setEv(value);"> 1.0
<input name="ev" type="radio" value=1.3 onclick="setEv(value);"> 1.3
<input name="ev" type="radio" value=1.7 onclick="setEv(value);"> 1.7
<input name="ev" type="radio" value=2.0 onclick="setEv(value);"> 2.0
<br>
<br>
<input type="button" value="Take Picture" onclick="takePicture()">
<script type="module">
import * as THREE from './three/build/three.module.js';
import { OrbitControls } from './three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from './three/examples/jsm/loaders/GLTFLoader.js';
import { RGBELoader } from './three/examples/jsm/loaders/RGBELoader.js';
import { RoughnessMipmapper } from './three/examples/jsm/utils/RoughnessMipmapper.js';
var container, controls;
var camera, scene, renderer;
var texture,image;
var textureLoader = new THREE.TextureLoader();
var options = {
generateMipmaps: true,
minFilter: THREE.LinearMipmapLinearFilter,
magFilter: THREE.LinearFilter
};
init();
function init() {
container = document.getElementById('container')
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
renderer.outputEncoding = THREE.sRGBEncoding;
container.appendChild( renderer.domElement );
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.25, 20 );
camera.position.set( - 1.8, 0.6, 0.7 );
scene = new THREE.Scene();
image = document.getElementById('lvimg');
texture = textureLoader.load(image.src);
texture.mapping = THREE.UVMapping;
var roughnessMipmapper = new RoughnessMipmapper( renderer );
scene.background = new THREE.WebGLCubeRenderTarget( 1920, options ).fromEquirectangularTexture( renderer, texture );
scene.environment = new THREE.WebGLCubeRenderTarget( 1920, options ).fromEquirectangularTexture( renderer, texture );
let mixer;
let clock = new THREE.Clock();
var loader = new GLTFLoader().setPath('models/DamagedHelmet/glTF/');
loader.load( 'DamagedHelmet.gltf', function ( gltf ) {
gltf.scene.traverse( function ( child ) {
} );
scene.add( gltf.scene );
roughnessMipmapper.dispose();
render();
} );
var pmremGenerator = new THREE.PMREMGenerator( renderer );
pmremGenerator.compileEquirectangularShader();
controls = new OrbitControls( camera, renderer.domElement );
controls.addEventListener( 'change', render ); // use if there is no animation loop
controls.minDistance = 0.5;
controls.maxDistance = 2;
controls.target.set( 0, 0, - 0.2 );
controls.update();
window.addEventListener( 'resize', onWindowResize, false );
animate();
}
function animate()
{
requestAnimationFrame( animate );
render();
}
function onWindowResize()
{
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
render();
}
function render()
{
textureLoader.load(image.src, function ( texture ) {
scene.background.fromEquirectangularTexture(renderer, texture);
scene.environment.fromEquirectangularTexture(renderer, texture);
});
renderer.render( scene, camera );
}
</script>
</body>
</html>
xmlHttpRequest.onreadystatechange = function() {
if (this.readyState === READYSTATE_COMPLETED &&
this.status === HTTP_STATUS_OK) {
var responseJson = JSON.parse(this.responseText);
if (responseJson.state=="done") {
var targetText = document.getElementById('img_url');
var TargetImage = responseJson.results.fileUrl;
getTpImage(TargetImage);
targetText.textContent = responseJson.results.fileUrl;
startLivePreview();
} else {
setTimeout("watchTpComplete()",5);
}
} else {
console.log('setOptions failed');
}
};
取得したURLは、html側の要素に保持しておきます。このために、index.htmlに以下を追加します。
<div hidden id="img_url"></div>
次に取得したURLをサーバーに渡し、画像を要求する関数を追加します。
var IMAGE_GET = 'TpImage/';
function getTpImage(fname) {
var command = {
fname,
};
var xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.open("GET", IMAGE_GET + fname, true);
xmlHttpRequest.responseType = 'blob';
xmlHttpRequest.send(null);
xmlHttpRequest.onload = function() {
var data = new Uint8Array(this.response);
var img = document.getElementById('TpImage');
img.src = URL.createObjectURL(this.response);
};
}
また、この要求に従い画像を返す仕組みをサーバー側に追加します。
追加箇所はserve関数内とTpImageSend関数追加の2点です。TpImageSend関数は、先程の画像URLを受け取り撮影画像を返す部分になります。
public Response serve(IHTTPSession session) {
~略
if( uri.startsWith("/osc/") ) {
//Divert THETA webAPI(OSC) commands.
return serveOsc(method, uri, postData);
} ~略
else if(uri.startsWith("/TpImage"))//追加部分
{
return TpImageSend(uri);
}
else
~略
}
public static final String DCIM = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getPath();
private Response TpImageSend(String uri) {
Matcher matcher = Pattern.compile("/\\d{3}RICOH.*").matcher(uri);
String fileUrl = "";
if (matcher.find()) {
fileUrl = DCIM + matcher.group();
Log.d(TAG,matcher.group());
}
File file = new File(fileUrl);
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
} catch (IOException e) {
e.printStackTrace();
}
String mimeType = "image/jpeg";
return newChunkedResponse(Response.Status.OK, mimeType, fis);
}
また、この画像読み込みのためにAndroidManifest.xmlにアクセス権限を追加します
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
これで、撮影画像を利用する準備も出来ました。あとは、これをライブプレビューと切り替えて利用できるUIを追加してやります。
このUIですが今回せっかくWebGLを利用しているので、その上で動作するUIライブラリdat.guiを利用します。
GitHubよりdat.gui.jsを持ってきて、他のJavaScriptと同じフォルダに入れて読み込みます。
使用方法は、以下の通りindex.html内でUIのボタン要素ごとに関数を割り当てるだけです。
var GuiElement = function () {
this.Take_Picture = function () { takePicture();}
this.Set_Picture = function (){setTpImage();}
this.Set_Live_Image = function (){setLvImage();}
};
var elements = new GuiElement();
var gui = new dat.GUI();
gui.add(elements, 'Take_Picture');
gui.add(elements, 'Set_Picture');
gui.add(elements, 'Set_Live_Image');
そして、それぞれの関数を定義すれば完成です。実装としては、ライブプレビューか撮影画像かの選択状態を"state"という要素に持たせ、それをrender時に確認しています。また、撮影画像の場合、1度読み込めば毎フレーム読み込む必要はないので、その確認もおこなっています。
function render()
{
var status = document.getElementById('state');
if(status.textContent== "1" && TpImageLoaded == false)
{
var Tpimg = document.getElementById('TpImage');
textureLoader.load(Tpimg.src, function ( texture ) {
scene.background.fromEquirectangularTexture(renderer, texture);
scene.environment.fromEquirectangularTexture(renderer, texture);
});
TpImageLoaded = true;
}
else if (status.textContent == "0")
{
textureLoader.load(image.src, function ( texture ) {
scene.background.fromEquirectangularTexture(renderer, texture);
scene.environment.fromEquirectangularTexture(renderer, texture);
});
}
renderer.render( scene, camera );
}
function setTpImage() {
var status = document.getElementById('state');
status.textContent = "1";
TpImageLoaded = false;
}
function setLvImage() {
var status = document.getElementById('state');
status.textContent = "0";
}
これで実装は完了です。試してみましょう。(初回起動時は、Vysorによる権限付与が必要になります)
一目瞭然で綺麗になりました。(角度が変わるのは天頂補正によるものです)
なお、今回使用しているglTFというファイル形式はアニメーションも対応しています。
glTFは3DにおけるJPEGを狙う次世代フォーマットなので、Blenderを始めとした多くのアプリケーションで作成が可能です。
検索するとフリーのモデルも多くありますので、ぜひ好みのCGでIBLしてみて下さい。
最後に、全文を掲載しておきます。
撮影画像表示対応のindex.html全文
<html>
<head>
<title>WebGL Image Based Lighting</title>
<meta charset="utf-8">
<script src="js/preview.js"></script>
<script src="js/three.min.js"></script>
<script src="js/dat.gui.js"></script>
</head>
<body onLoad="startLivePreview();updatePreviwFrame();">
<div id="container"> </div>
<img hidden id="lvimg" src="" width="640" height="320">
<img hidden id="TpImage" src="" width="1024" height="512">
<br>
<br>
LivePreview
<br>
<input name="preview" type="radio" value="off" onclick="stopLivePreview();"> OFF
<input name="preview" type="radio" value="on" onclick="startLivePreview();"> ON
<br>
<br>
setEv
<br>
<input name="ev" type="radio" value=-2.0 onclick="setEv(value);"> -2.0
<input name="ev" type="radio" value=-1.7 onclick="setEv(value);"> -1.7
<input name="ev" type="radio" value=-1.3 onclick="setEv(value);"> -1.3
<input name="ev" type="radio" value=-1.0 onclick="setEv(value);"> -1.0
<input name="ev" type="radio" value=-0.7 onclick="setEv(value);"> -0.7
<input name="ev" type="radio" value=-0.3 onclick="setEv(value);"> -0.3
<input name="ev" type="radio" value=0.0 onclick="setEv(value);"> 0.0
<input name="ev" type="radio" value=0.3 onclick="setEv(value);"> 0.3
<input name="ev" type="radio" value=0.7 onclick="setEv(value);"> 0.7
<input name="ev" type="radio" value=1.0 onclick="setEv(value);"> 1.0
<input name="ev" type="radio" value=1.3 onclick="setEv(value);"> 1.3
<input name="ev" type="radio" value=1.7 onclick="setEv(value);"> 1.7
<input name="ev" type="radio" value=2.0 onclick="setEv(value);"> 2.0
<br>
<br>
<div hidden id="img_url"></div>
<div hidden id="state">0</div>
<script type="module">
import * as THREE from './three/build/three.module.js';
import { OrbitControls } from './three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from './three/examples/jsm/loaders/GLTFLoader.js';
import { RGBELoader } from './three/examples/jsm/loaders/RGBELoader.js';
import { RoughnessMipmapper } from './three/examples/jsm/utils/RoughnessMipmapper.js';
var container, controls;
var camera, scene, renderer;
var texture,image;
var state = false;
var TpImageLoaded = false;
var textureLoader = new THREE.TextureLoader();
var options = {
generateMipmaps: true,
minFilter: THREE.LinearMipmapLinearFilter,
magFilter: THREE.LinearFilter
};
init();
function init() {
container = document.getElementById('container')
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
renderer.outputEncoding = THREE.sRGBEncoding;
container.appendChild( renderer.domElement );
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.25, 20 );
camera.position.set( - 1.8, 0.6, 0.7 );
scene = new THREE.Scene();
image = document.getElementById('lvimg');
texture = textureLoader.load(image.src);
texture.mapping = THREE.UVMapping;
var roughnessMipmapper = new RoughnessMipmapper( renderer );
scene.background = new THREE.WebGLCubeRenderTarget( 1920, options ).fromEquirectangularTexture( renderer, texture );
scene.environment = new THREE.WebGLCubeRenderTarget( 1920, options ).fromEquirectangularTexture( renderer, texture );
let mixer;
let clock = new THREE.Clock();
var loader = new GLTFLoader().setPath('models/DamagedHelmet/glTF/');
loader.load( 'DamagedHelmet.gltf', function ( gltf ) {
gltf.scene.traverse( function ( child ) {
} );
scene.add( gltf.scene );
roughnessMipmapper.dispose();
render();
} );
var pmremGenerator = new THREE.PMREMGenerator( renderer );
pmremGenerator.compileEquirectangularShader();
var GuiElement = function () {
this.Take_Picture = function () { takePicture();}
this.Set_Picture = function (){setTpImage();}
this.Set_Live_Image = function (){setLvImage();}
};
var elements = new GuiElement();
var gui = new dat.GUI();
gui.add(elements, 'Take_Picture');
gui.add(elements, 'Set_Picture');
gui.add(elements, 'Set_Live_Image');
controls = new OrbitControls( camera, renderer.domElement );
controls.addEventListener( 'change', render ); // use if there is no animation loop
controls.minDistance = 0.5;
controls.maxDistance = 2;
controls.target.set( 0, 0, - 0.2 );
controls.update();
window.addEventListener( 'resize', onWindowResize, false );
animate();
}
function animate()
{
requestAnimationFrame( animate );
render();
}
function onWindowResize()
{
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
render();
}
function render()
{
var status = document.getElementById('state');
if(status.textContent== "1" && TpImageLoaded == false)
{
var Tpimg = document.getElementById('TpImage');
textureLoader.load(Tpimg.src, function ( texture ) {
scene.background.fromEquirectangularTexture(renderer, texture);
scene.environment.fromEquirectangularTexture(renderer, texture);
});
TpImageLoaded = true;
}
else if (status.textContent == "0")
{
textureLoader.load(image.src, function ( texture ) {
scene.background.fromEquirectangularTexture(renderer, texture);
scene.environment.fromEquirectangularTexture(renderer, texture);
});
}
renderer.render( scene, camera );
}
function setTpImage() {
var status = document.getElementById('state');
status.textContent = "1";
TpImageLoaded = false;
}
function setLvImage() {
var status = document.getElementById('state');
status.textContent = "0";
}
</script>
</body>
</html>
#まとめ
今回は、THETA1台(+スマホ)でIBLする方法を紹介しました。
THETA側にサーバーを立ててWebGLを利用することで、全天球画像を利用したインタラクティブなアプリケーションも簡単かつスタンドアロンに構築することが出来ます。ぜひ、お試し下さい。
#RICOH THETAプラグインパートナープログラムについて
THETAプラグインをご存じない方はこちらをご覧ください。
パートナープログラムへの登録方法はこちらにもまとめてあります。
QiitaのRICOH THETAプラグイン開発者コミュニティ TOPページ「About」に便利な記事リンク集もあります。
興味を持たれた方はTwitterのフォローとTHETAプラグイン開発コミュニティ(Slack)への参加もよろしくおねがいします。