Hatena::Groupjavascript

JavaScriptで遊ぶよ

2011-12-22

WebGL best practices

20:12

MDNにこういうドキュメントがあると教えてもらったので翻訳してみます。

元のライセンスは"Creative Commons: Attribution-Sharealike license"で、僕の翻訳について著作権は主張しません。(なので https://developer.mozilla.org/ja/ に転載したい人がいればご自由に)

この文書はWebGLを使ったコンテンツの向上のためのTipsについて書きます。これらの提案に従うことで、多くの機器への互換性を高めたり、パフォーマンスを上げることにもなります。

避けたほうがいいこと

  • WebGLのエラーを出さないように注意しましょう。エラーはgetError()で得られますが、Firefoxではwebgl.verboseの設定を有効にすることで、ウェブコンソールにWebGLのエラーと警告を出力します。ユーザーのコンソールにエラーを吐き出す必要はないでしょう?(訳註:パフォーマンスの理由もある。下参照)
  • #ifdef GL_ESは絶対に使ってはいけません。初期の例ではこれが使われていましたが、WebGLでは必ずtrueになるので必要ありません。
  • フラグメントシェーダでhighp精度を使うのはやめましょう。mediumpを代わりに使いましょう。highpを使うと今のモバイルのハードウェアのほとんどで動きません。Firefox 11からはgetShaderPrecisionFormat()関数が実装されるので、highp精度が使えるかどうかだけでなく、それぞれの精度の名称について実際の精度を知ることができます。

覚えておいたほうがいいこと

  • WebGLの機能の中にはクライアントによって制限があるものがあります。そういうものを使う前にはgetParameter()を使って調べましょう。例えば、2Dテクスチャのサイズはwebgl.getParameter(webgl.MAX_TEXTURE_SIZE)でわかります。Firefox 10からはwebgl.min_capability_modeの設定があり、最低限の機能の環境をシミュレートすることができます。
  • 特に、頂点シェーダでのテクスチャの使用はwebgl.getParameter(webgl.MAX_VERTEX_TEXTURE_IMAGE_UNITS)がゼロより大きくなければ使えません。現在のモバイルのハードウェアではまず使えないでしょう。
  • WebGLの拡張が使えるかどうかはクライアントに依存します。できるならそれらの使用をオプションとし、サポートされていない環境にも対応できるようにしましょう。Firefox 10からはwebgl.disable-extensionsの設定があり、拡張のない環境をシミュレートすることができます。
  • OES_texture_float拡張がサポートされていたとしても、浮動小数点数テクスチャへのレンダリングはサポートされていないかもしれません。モバイルのハードウェアではまず動かないでしょう。サポートされているかを調べるにはcheckFramebufferStatus()を使ってください。

一般的なパフォーマンスのtips

  • CPUとGPUの同期を必要とするものはすべて、とても遅い可能性があり、メインのレンダリングループでは避けたほうがいいでしょう。getError()readPixelsfinish()などの関数がそれです。getParameter()getUniformLocation()といったWebGLのゲッタも遅いので、JS側で変数にキャッシュしてください。
  • 大きい描画を少数だけしたほうがパフォーマンスが向上します(訳註:小さな描画をたくさん行うより)。1000回の小さなものを描画するなら、一回のdrawArrays()drawElements()でやりましょう。一回のdrawArrays()で離れたオブジェクトを描画するなら、3点が一直線上にある三角形が使えます。
  • 状態の変更が少ないほどパフォーマンスが向上します。特に、複数の画像をひとつのテクスチャにまとめて適切な座標を使うことでバインドしているテクスチャの変更が少なくて済みます。
  • 小さなテクスチャは大きなテクスチャよりパフォーマンスが良いです。そのためmipmapが有効です。
  • 簡単なシェーダーは複雑なものよりもパフォーマンスが良いです。特に、if文を減らせば速くなります。割り算やlog()などの数学の演算もコストが高いです。

2011-12-15

ammo.jsとbullet.jsを使ってみた(bullet.js編)

09:46

JavaScript Advent Calendar 2011 WebGL駅伝16日目です。

14日目はammo.jsを試しました。比較しながらお読みください。ちなみに15日目は@Roxigaさんによる自作ライブラリPsycoVision3Dの紹介でした。

bullet.jsは↓のデモのHTMLの中に直接書かれています。もう一つ前のデモもありましたが、その中のコードとは微妙に別物でした。

この中でVecmathとBulletという2つの名前空間を定義しています。VecmathはBulletに渡すためのベクトルや行列のクラス群です。


それではammo.jsでやったのと同じサンプルを使って説明していきます。

f:id:edvakf:20111216094643p:image:w700


物理世界

今日はまず物理世界を作るところからやります。

function initPhysicsWorld() {
  var gravity = new Vecmath.Vec3(0, -200, 0);

  var collisionConfiguration = new Bullet.CollisionConfiguration();
  var dispatcher = new Bullet.Dispatcher(collisionConfiguration);

  var worldAabbMin = new Vecmath.Vec3(-1500, -1500, -1500);
  var worldAabbMax = new Vecmath.Vec3(1500, 1500, 1500);

  var overlappingPairCache = new Bullet.BroadphaseInterface(
                worldAabbMin, worldAabbMax, 0xfffe, 0xffff, 16384, null);

  var solver = new Bullet.ConstraintSolver();
  dynamicsWorld = new Bullet.CollisionWorld(
    dispatcher, overlappingPairCache, solver, collisionConfiguration);
  dynamicsWorld.setGravity(gravity);

  return dynamicsWorld;
}

ammo.js版↓と見比べてみます。

function initPhysicsWorld() {
  var gravity = new Ammo.btVector3(0, -200, 0);

  var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
  var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
  var overlappingPairCache = new Ammo.btDbvtBroadphase();
  var solver = new Ammo.btSequentialImpulseConstraintSolver();
  var dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(
      dispatcher, overlappingPairCache, solver, collisionConfiguration);
  dynamicsWorld.setGravity(gravity);

  return dynamicsWorld;
}

ammo.jsのAmmo.btDbvtBroadphaseということろがbullet.jsではBullet.BroadphaseInterfaceになって、何やらworldAabbMinとかworldAabbMaxというのが増えてます。引数の意味はよく知りません。

どちらも同じbtDbvtBroadphaseに相当するようですが、bullet.jsのほうはコンストラクタの後にcreateProxyメソッドを呼んでるのに相当する処理をBullet.BroadphaseInterfaceだけで行なっているようです。

ちなみにBroadphaseというのは、近くにある剛体同士をざっくりとグループ分けする段階です。厳密な衝突判定はその後で行います。overlappingPairCacheは近くにある剛体同士のペアのキャッシュだったのですね。ってなことは今日知りました。


他のやつも名前が微妙に違ったりしてますね。jBulletの影響なんでしょうか。


剛体

bullet.jsはこんな感じです。

Box.prototype.initBulletObj = function(m) {
  var s = this.s;
  var shape = new Bullet.BoxShape(new Vecmath.Vec3(s / 2, s / 2, s / 2));
  var startTransform = new Bullet.Transform();
  startTransform.setIdentity();
  startTransform.origin.set3(this.x, this.y, this.z);

  var localInertia = new Vecmath.Vec3(0, 0, 0);
  var motionState = new Bullet.MotionState(startTransform);
  var rbInfo = new Bullet.RigidBodyConstructionInfo(m, motionState, shape, localInertia);
  rbInfo.restitution = 1;
  var body = new Bullet.RigidBody(rbInfo);

  this.bulletObj = body;
};

このへんはammo.jsのやり方↓とほとんど変わりません。ただ、newしても自分で解放しなくてもいいというのはかなりラクです。

Box.prototype.initBulletObj = function(m) {
  var s = this.s;
  var tmpVec = new Ammo.btVector3(s / 2, s / 2, s / 2);
  var shape = new Ammo.btBoxShape(tmpVec);
  Ammo.destroy(tmpVec);
  var startTransform = new Ammo.btTransform();
  startTransform.setIdentity();
  tmpVec = new Ammo.btVector3(this.x, this.y, this.z);
  startTransform.setOrigin(tmpVec);
  Ammo.destroy(tmpVec);

  var localInertia = new Ammo.btVector3(0, 0, 0);
  var motionState = new Ammo.btDefaultMotionState(startTransform);
  var rbInfo = new Ammo.btRigidBodyConstructionInfo(m, motionState, shape, localInertia);
  rbInfo.set_m_restitution(1);
  var body = new Ammo.btRigidBody(rbInfo);

  Ammo.destroy(startTransform);
  //Ammo.destroy(shape)
  Ammo.destroy(localInertia);
  //Ammo.destroy(motionState);
  Ammo.destroy(rbInfo);

  this.bulletObj = body;
};

moveメソッドもほとんど同じですが、bullet.jsのほうはgetMotionStateがありません。これもjBulletの作法なんでしょうか?

//bullet.js
  this.bulletObj.getWorldTransform(this._trans);
  this.x = this.threeObj.position.x = this._trans.origin.x;
  this.y = this.threeObj.position.y = this._trans.origin.y;
  this.z = this.threeObj.position.z = this._trans.origin.z;

//ammo.js
  this.bulletObj.getMotionState().getWorldTransform(this._trans);
  var origin = this._trans.getOrigin();
  this.x = this.threeObj.position.x = origin.x();
  this.y = this.threeObj.position.y = origin.y();
  this.z = this.threeObj.position.z = origin.z();

このように、ところどころ違うところがあります。


結果

お試しあれ。


気づいたいくつか。

まず、ammo.jsに比べるとまだまだ圧倒的に貧弱です。実はbtSphereShape相当のものが無くて、仕方なくBullet.CapsuleShapeで代用しています。他にもremoveRigidBodyが無いので画面外のボールを取り除くことが出来なかったり。

そして、ボールが近づきすぎるとエラーが出ます。

f:id:edvakf:20111216073202p:image

上のデモはたぶん3分もたたずに止まっちゃうと思います。ランダムですが。

早い話が、使える品質になるまではまだまだかかりそうでした。というかメンテされてるわけでもなさそうですし、自分でやる以外には期待できません。まああれだけのコードを移植したpl4n3さんがすごいのは間違いありませんが。

JSのGCの恩恵に預かれるので書き心地はammo.jsに比べてだいぶラクなんですけどね。


それから、dynamicsWorld.stepSimulation1はC++では1引数版と3引数版(と2引数版)がオーバーロードされていますが、bullet.jsではstepSimulation1stepSimulation3というふうに分かれていました。

そんなところでした。

トラックバック - http://javascript.g.hatena.ne.jp/edvakf/20111215

2011-12-13

ammo.jsとbullet.jsを使ってみた(ammo.js編)

03:53

JavaScript Advent Calendar 2011 WebGL駅伝14日目です。

前に紹介だけしたbullet.jsとammo.jsを試してみたので使い心地を書いてみます。


まずこんな感じでThree.jsを使ったコードがあるとします。まだ動きません。一回描画するだけです。

f:id:edvakf:20111214025452p:image:w500:right

window.addEventListener("load", function(){
  var width = 800;
  var height = 600;

  var camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
  camera.position.x = -200;
  camera.position.y = 200;
  camera.position.z = 1000;
  camera.lookAt(new THREE.Vector3(0, 0, 0));

  var scene = new THREE.Scene();

  var directionalLight = new THREE.DirectionalLight( 0xffffff, 3 );
  directionalLight.position.z = 3;
  scene.add( directionalLight );

  var geometry = new THREE.CubeGeometry(400, 400, 400);
  var material = new THREE.MeshLambertMaterial({color: 0x660000});
  var ground = new THREE.Mesh(geometry, material);
  scene.add(ground);
  ground.position.y -= 250;

  for (var i = 0; i < 20; i++) {
    var geometry = new THREE.SphereGeometry(20);
    var material = new THREE.MeshLambertMaterial({color: 0x666666});
    var ball = new THREE.Mesh(geometry, material);
    ball.position.y += i * 40;
    scene.add(ball);
  }

  var renderer = new THREE.WebGLRenderer();
  renderer.setSize(width, height);
  document.body.appendChild(renderer.domElement);

  renderer.render(scene, camera);

}, false);

これを動かしてみたいと思います。

今日はammo.jsだけ。明日bullet.jsのほうをやりたいと思います。


剛体を作る

基本的にはThree.jsのオブジェクト一つにつきbulletのオブジェクト(剛体)を一つ作ることになるので、面倒なので両方を束ねるオブジェクトを作ります。

まずはボールのオブジェクトから。

function Ball(x, y, z, r, m) {
  this.x = x;
  this.y = y;
  this.z = z;
  this.r = r;
  this.bulletObj = null;
  this.threeObj = null;
  this._trans = new Ammo.btTransform();
  this.initThreeObj();
  this.initBulletObj(m);
}

Ball.prototype.destructor = function() {
  Ammo.destroy(this.bulletObj);
  Ammo.destroy(this._trans);
};

こんな感じです。rは半径、mは質量です。destructorってなんで必要なの?ってのはあとで説明します。

initThreeObjはほぼ上で書いたコードのコピペです。

Ball.prototype.initThreeObj = function() {
  var geometry = new THREE.SphereGeometry(this.r);
  var material = new THREE.MeshLambertMaterial({color: 0x666666});
  var ball = new THREE.Mesh(geometry, material);
  ball.position.x = this.x;
  ball.position.y = this.y;
  ball.position.z = this.z;

  this.threeObj = ball;
};

こっからが本番。bulletのオブジェクトを作ります。

Ball.prototype.initBulletObj = function(m) {
  var startTransform = new Ammo.btTransform();
  startTransform.setIdentity();
  var origin = startTransform.getOrigin();
  origin.setX(this.x);
  origin.setY(this.y);
  origin.setZ(this.z);

  var shape = new Ammo.btSphereShape(this.r);
  var localInertia = new Ammo.btVector3(0, 0, 0);
  shape.calculateLocalInertia(m, localInertia);

  var motionState = new Ammo.btDefaultMotionState(startTransform);
  var rbInfo = new Ammo.btRigidBodyConstructionInfo(m, motionState, shape, localInertia);
  rbInfo.set_m_restitution(1);
  var body = new Ammo.btRigidBody(rbInfo);

  Ammo.destroy(startTransform);
  //Ammo.destroy(shape);
  Ammo.destroy(localInertia);
  //Ammo.destroy(motionState);
  Ammo.destroy(rbInfo);

  this.bulletObj = body;
};

めんどくさいですね。startTransformというのは初期位置を表します。bulletでは剛体の座標から世界の座標への変換(ベクトルとクォータニオン)という形で剛体の位置やら回転を表します。btSphereShapeで形を作って、それを元にbtRigidBodyConstructionInfoで剛体を生成するための情報を作って、btRigidBodyでようやく剛体を作ります。

Ammo.destoryのことはあとで説明します。

で、ボールを移動させた後にbulletのオブジェクトから移動後の位置や回転を読んでThree.jsのオブジェクトを動かしてやります。

Ball.prototype.move = function() {
  this.bulletObj.getMotionState().getWorldTransform(this._trans);
  var origin = this._trans.getOrigin();
  this.x = this.threeObj.position.x = origin.x();
  this.y = this.threeObj.position.y = origin.y();
  this.z = this.threeObj.position.z = origin.z();
  var quaternion = this._trans.getRotation();
  var x = quaternion.x();
  var y = quaternion.y();
  var z = quaternion.z();
  var w = quaternion.w();
  this.threeObj.rotation.x = Math.atan2(2*(x*y + w*z), w*w + x*x - y*y - z*z); // roll
  this.threeObj.rotation.y = Math.atan2(2*(y*z + w*x), w*w - x*x - y*y + z*z); // pitch
  this.threeObj.rotation.z = Math.asin(-2*(x*z - w*y)); // yaw
};

地面のほうはまあほぼ同じなのでいいと思います。地面は質量にゼロを与えることで「動かない」(質量無限大の)オブジェクトが作れます。それから、Three.jsのオブジェクトを作るときに1辺が400の箱をこうやって作ったのですが、

  var geometry = new THREE.CubeGeometry(400, 400, 400);

bulletの引数は1辺の半分の長さになります。

  var tmpVec = new Ammo.btVector3(s / 2, s / 2, s / 2);
  var shape = new Ammo.btBoxShape(tmpVec);
  Ammo.destroy(tmpVec);

物理演算の世界を作る

剛体を作るのも面倒でしたが、世界を作るのも面倒です。

function initPhysicsWorld() {
  var gravity = new Ammo.btVector3(0, -200, 0);

  var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
  var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
  var overlappingPairCache = new Ammo.btDbvtBroadphase();
  var solver = new Ammo.btSequentialImpulseConstraintSolver();
  var dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(
      dispatcher, overlappingPairCache, solver, collisionConfiguration);
  dynamicsWorld.setGravity(gravity);

  return dynamicsWorld;
}

重力はいいとして、他のやつはよくわかりません。どっかのデモコードからコピペしました。こういう作法みたいです。


動かす

準備完了しました。あとは動かすだけです。初期化の部分は一番上のコードとほとんど同じです。

window.addEventListener("load", function(){
  var width = 800;
  var height = 600;
  var deltaT = 30;

  var camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
  camera.position.x = -200;
  camera.position.y = 200;
  camera.position.z = 1000;
  camera.lookAt(new THREE.Vector3(0, 0, 0));

  var scene = new THREE.Scene();

  var directionalLight = new THREE.DirectionalLight( 0xffffff, 3 );
  directionalLight.position.z = 3;
  scene.add(directionalLight);

  var dynamicsWorld = initPhysicsWorld();

  var ground = new Box(0, -250, 0, 400, 0);
  scene.add(ground.threeObj);
  dynamicsWorld.addRigidBody(ground.bulletObj);

  var numballs = 0;
  var balls = [];
  for (var i = 0; i < 20; i++) {
    var ball = new Ball(Math.random(), i * 40, Math.random(), 20, 10);
    scene.add(ball.threeObj);
    dynamicsWorld.addRigidBody(ball.bulletObj);
    balls.push(ball);
    numballs++;
  }

  var renderer = new THREE.WebGLRenderer();
  renderer.setSize(width, height);
  document.body.appendChild(renderer.domElement);

  var n = 0;
  function rendering() {
    dynamicsWorld.stepSimulation(deltaT / 1000);

    for (var i = numballs; i--; ) {
      var ball = balls[i];
      ball.move();

      if (ball.y < -1000) {
        scene.remove(ball.threeObj);
        dynamicsWorld.removeRigidBody(ball.bulletObj);
        ball.destructor();
        balls.splice(i, 1);
        numballs--;
      }
    }

    if (numballs < 20) {
      var ball = new Ball(Math.random() * 10, Math.random() * 20 * 40, Math.random() * 10, 20, 10);
      scene.add(ball.threeObj);
      dynamicsWorld.addRigidBody(ball.bulletObj);
      balls.push(ball);
      numballs++;
    }

    renderer.render(scene, camera);
    setTimeout(rendering, deltaT);
  }

  rendering();

}, false);

rendering関数の最初にあるdynamicsWorld.stepSimulationが物理演算をちょこっと進めるメソッドです。単位は秒なので、1000で割ってます。

その次は、ボールごとにball.moveを呼んで、もしy座標が-1000より小さくなったら取り除いています。さらに、ボールの数が20個を下回ったら順次新しいボールを足しています。


まとめ

デモ↓Chromeで開くと軽くブラクラになります。Firefoxで開いて下さい。

結論から言うと、ammo.jsはもうちょっとすれば良くなりそうという感じです。最大の難点が、ChromeのJSエンジンと相性が悪いので、ammo.jsをパースするのに10秒もかかってしまうということです。Firefoxなら一瞬です。あとChromeのGCが数百msに一回走るため、そこで動きがカク、カク、となってしまいます。(そういえばChromiumに新しいGCが入ったとかどうとか…期待!)

f:id:edvakf:20111210160808p:image:w800

↑Three.jsとammo.jsを比べてみてください。↓内訳。何やらいっぱいGCしています。

f:id:edvakf:20111210162239p:image:w700


それからammo.jsのオブジェクトは必ずnewで作らなければいけません。そしてnewしたものは手動でdestroyしなければいけません。

なんだC++と同じじゃん、と思う無かれ、↓こういうふうにはできません。

  var shape = new Ammo.btBoxShape(Ammo.btVector3(s / 2, s / 2, s / 2));

↓こうしないとメモリリークします。(その前にbtVector3new無しでは呼べません)

  var tmpVec = new Ammo.btVector3(s / 2, s / 2, s / 2);
  var shape = new Ammo.btBoxShape(tmpVec);
  Ammo.destroy(tmpVec);

オブジェクトは全部ヒープへ。スタックにはポインタしか積めないプログラミングが味わえます。

注意して全部解放しようと思ったのですが、↓この2つだけはエラーが出ました。どうしてなんでしょうか。

  Ammo.destroy(shape);
  Ammo.destroy(motionState);

それで、上のデモを走らせ続けると案の定メモリを食い続けます。まあメモリリークと言ってもタブを閉じると解放される類のものですけど。このデモみたいにオブジェクトを追加し続けるようなものには向かないでしょう。

最後に、Readmeにも書いてありますが、ammo.jsは高速化のために全部のオブジェクトをグローバルに宣言します。クロージャとか使わないで。これだけで50%も速くなるそうです。今のところAmmowindowと同じです。これを付けずに使うこともできますが、将来はもしかしたらクロージャにするかもしれないのでつけたほうがいいそうです。もしくはworkerにしたほうがいいでしょう。

以上です。明日はbullet.jsをやります。


余談ですがこれのデモのために初めてThree.jsを使いました。簡単すぎて泣きそうでした。

2011-12-09

CompositionEventの紹介

00:49

JavaScript Advent Calendar 2011オレ標準駅伝10日目です。それにしてもみなさん濃いですね。圧巻はoogattaさんでしょうか。

今日は軽めに、あまり知られてないと思われるDOMのAPIを紹介したいと思います。

DOM3 EventsにはComposition Event Typesというのがあります。

CompositionEventにはcompositionstart, compositionupdate, compositionendの3つのイベントタイプがありまして、これらはIME専用のイベントタイプです。

ChromeとFirefoxとIE9では既に使えました。デモを試してみてください。

f:id:edvakf:20111209062313p:image


こんな感じになるわけです。

IMEを使って最初の一文字を入力しようとするときに出るのがcompositionstartで、キーを打ってるときと変換候補を巡ってるときに出るのがcompositionupdateで、決定のときに出るのがcompositionendです。


何が嬉しいの?

ChromeやIEはIMEで入力中にもkeyupやkeydownが出ますが(昔は違ったような気が…うろ覚え)、FirefoxやOperaなどはCompositionEvent以外には何もイベントを出さないので、compositionupdateが無ければIMEで入力中なのかどうかわかりません。Firefoxではtextというオレ標準イベントが出るらしいです。Operaは東アジア対応が弱いので、CompositionEventが近いうちに入る可能性は低い気がします。

なので、サジェスト系のUIだとタイマーで入力欄のvalueを見てやらないといけません。

CompositionEventがあればもうそんな心配はいりませんね。


それから、↓こういう経験ありますよね。

f:id:edvakf:20111209063950p:image

compositionstartがcancelableになっているのはこれを解決しようとしてのことだと思います。標準IMEをキャンセルしちゃってキー打つごとにサジェスト出したらいいじゃん、って感じです。

しかし、今のところcompositionstartをpreventDefaultできるのはIE9だけでした。MozillaのMDNにはこう書かれています。

Note: This event should handle starting the text composition system, but in Gecko it's the other way around; when the system starts its composition system, Gecko fires this event.

Gecko notes

According to the DOM Level3 specification, compositionstart is cancelable; however, Gecko doesn't currently let you cancel them.

Gecko fires this event when IME starts composition, and some platforms don't have an API for canceling composition once it's begun. In addition, Gecko can't know whether a keyboard event will start composition or not until IME actually starts composition. Because of this, event.preventDefault() doesn't work on compositionstart events in Gecko.

compositionstart - MDN

たぶんChromeも似たような理由でcancelableにしていないんだと思います。

標準のIMEをスタートさせないって、ユーザビリティとしてはどうなのかなとも思いますけどねぇ。


もうOSのIMEなんかに頼らずウェブサイト側がIMEを提供しちゃえばいいんじゃね?みたいなぶっ飛んだ提案をGoogle Chromeチームの人が出してたこともあります。(ウェブサイト側というよりChrome OSやExtensionのことを睨んだものかもしれませんが)

ここから続くスレッドで、こっちに飛んで、最後はここまで続いて、まあ否定的に立ち消えになってます。


CompositionEventの話から逸れました。

Chromeの実装でおかしなところを見つけたのでこれも紹介しておきます。

Some implemenations may populate the data attribute of the compositionstart event with the text currently selected in the document (for editing and replacement); otherwise, the value of the data attribute must be the empty string.

Document Object Model (DOM) Level 3 Events Specification

仕様によると、compositionstartでは.dataが空文字か、またはIMEで入力しようとするときに選択中でこれから置き換えられるはずのテキストにせよと書いてあるんですが、Chromeではcompositionupdateと同じ、つまり最初の文字が入ってます。

その他、他のイベントとの順番や、イベントリスナー中でtextarea.valueを取ったときの値などを見るといろいろ食い違ってるので、やっぱりキーイベントは鬼門だなと思った次第です。


あ、そうそう、DOM3 Eventsのキーイベントでもう一つの大きな目玉といえば、.keyですよね。keyIdentifierを置き換えたあれです。これもIE9ではしっかり実装されてました。ChromeとFirefoxはまだです。あとgetModifierStateというメソッドe.shiftKeyとかの代わりにe.getModifierState('Shfit')とか書けるやつ)もIE9では実装されてました。すごい。

keyIdentifierやkey valueについてはこのブログに昔書いたことがありますので参考までに。


また話が逸れました。

まあそういうことで、今のところはサジェストでタイマー走らせないでいいよってぐらいしか良い所がなさそうなCompositionEventですが、その他の使い道を思いついた人は教えてください。

それでは、Happy Christmas!

nanto_vinanto_vi2011/12/10 01:14> FirefoxやOperaなどはCompositionEvent以外には何もイベントを出さない

一応Mozillaにはtextイベントなるものがあるそうです。
http://tech.kayac.com/archive/firefox-text-event.html

edvakfedvakf2011/12/10 01:26ありがとうございます。デモをアップデートしておきました。

2011-12-07

JavaScript Advent Calendar/WebGLコース7日目・WebGLとOpenGLの相違点

23:32

JavaScript Advent Calendar 2011 WebGL駅伝7日目です。


WebGLのwikiにこういうドキュメントがあったので翻訳してみたいと思います。

WebGLとOpenGLの相違点

WebGLはOpenGL ES 2.0の仕様に基いていて、モバイル機器へのポータビリティを最大化するようにOpenGL ESのセマンティックスを残しています。しかし、OpenGL ES 2.0とデスクトップ用OpenGLの似たAPIには大きな違いがあるものもります。

2の累乗でない(Non-Power of Two; NPOT)テクスチャーのサポート

デスクトップ用OpenGL 2.0以降ではNPOTなテクスチャーがサポートされていますが、OpenGL ES 2.0やWebGLでは限定的にしかサポートされていません。この制限は仕様(PDF)のセクション3.8.2("Shader Execution")とセクション3.7.11("Mipmap Generation")に書かれています。要点は次のとおりです。

  • generateMipmap(target)は、現在のテクスチャーがバインドされているレベル0画像の幅と高さがNPOTである場合、INVALID_OPERATIONエラーを出す。
  • NPOTなテクスチャーをサンプリングすると、以下の場合を除き、RGBA(0,0,0,1)を返す。
    • minification filterがNEARESTまたはLINEARにセットされている。(すなわちmipmapフィルターだと(0,0,0,1))
    • repeat modeがCLAMP_TO_EDGEにセットされている。(リピートするNPOTテクスチャーはサポートされていない)

もしREPEAT wrap modeを必要とせず、mipmapが無くても構わないなら、WebGLTextureオブジェクトを作るときに次のようにすればいいでしょう。

var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

しかし、もしREPEAT wrap modeを使う必要があるなら、DOM APIを使って画像を次の2の累乗にリサイズするのは簡単なことです。次の例がそれです。imageはロード済みの(onloadが既に呼ばれている)画像です。

function createTextureFromImage(image) {
    var texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    if (!isPowerOfTwo(image.width) || !isPowerOfTwo(image.height)) {
        // Scale up the texture to the next highest power of two dimensions.
        var canvas = document.createElement("canvas");
        canvas.width = nextHighestPowerOfTwo(image.width);
        canvas.height = nextHighestPowerOfTwo(image.height);
        var ctx = canvas.getContext("2d");
        ctx.drawImage(image,
                      0, 0, image.width, image.height,
                      0, 0, canvas.width, canvas.height);
        image = canvas;
    }
    gl.texImage2D(gl.TEXTURE_2D, 0, image);
    gl.generateMipmap(gl.TEXTURE_2D);
    gl.bindTexture(gl.TEXTURE_2D, null);
    return texture;
}
 
function isPowerOfTwo(x) {
    return (x & (x - 1)) == 0;
}
 
function nextHighestPowerOfTwo(x) {
    --x;
    for (var i = 1; i < 32; i <<= 1) {
        x = x | x >> i;
    }
    return x + 1;
}

もちろんサーバー側で画像をリサイズすることもできます。

0番目の頂点アトリビュート

デスクトップ用のOpenGLでは、0番目の頂点アトリビュートは特別な意味を持ちます。第一に、0番目の頂点アトリビュートは配列として定義されていなければいけません。そうでなければ何も描画されません。第二に、0番目の頂点アトリビュートはpersistent stateを持ちません。すなわち、glGetVertexAttribfv(0, GL_CURRENT_VERTEX_ATTRIB, ...)はエラーになります。

OpenGL ES 2.0では0番目の頂点アトリビュートは何も特別な意味を持ちません。

WegGLはOpenGL ES 2.0の慣習に従うため、すべての頂点アトリビュートは同じように扱えます。このためデスクトップのOpenGL実装はエミュレーションが必要になってきます。しかし、このコストは一貫性のある挙動に比べて十分に小さいと判断されました。

倍精度浮動小数点数のサポートはありません

OpenGL ES 2.0は頂点アトリビュートにおいてもテクスチャーデータにおいてもGL_DOUBLE型をサポートしません。つまりFloat64ArrayはWebGLにおいては現在のところ意味を成さないということです。

3Dテクスチャーのサポートはありません

OpenGL ES 2.0は3Dテクスチャーをサポートしません

texture2DLod

後ろにLodのついた関数(texture2DLodなど)は頂点シェーダーでしか使えません。


感想

前に↓を書いたときは一辺が2の累乗の「正方形」じゃないといけないと思ってましたが、長方形もいいみたいです。それから、サイズが大きすぎるとメモリを圧迫するかと思って「辺の長さを超えない最大の2の累乗」に縮小しましたが、上のコードでは「辺の長さを超える最小の2の累乗」に拡大しています。どっちでもいいと思いますが、拡大するほうが情報のロスがなくていいかもしれません。


0番目の頂点アトリビュートが特殊というのは例を見ないとどういうことなのかわかりかねますね。


ここに挙げられてるのはあくまで「似てるけど違う点」であって、その他にもOpenGLで検索してWebGLでも同じ手を使おうと思ったらできなかったということはけっこうあります。GLSLのgl_***という組み込み変数もかなり限られています。GLSL ESの仕様(PDF)の1.1 Change Historyというところを見ると例えば

  • The output variables gl_ClipVertex and gl_FragDepth are removed.

というふうに。

それからパラメーターの設定ではWebGLだけに定義されているものがあります。

    /* WebGL-specific enums */
    const GLenum UNPACK_FLIP_Y_WEBGL            = 0x9240;
    const GLenum UNPACK_PREMULTIPLY_ALPHA_WEBGL = 0x9241;
    const GLenum CONTEXT_LOST_WEBGL             = 0x9242;
    const GLenum UNPACK_COLORSPACE_CONVERSION_WEBGL = 0x9243;
    const GLenum BROWSER_DEFAULT_WEBGL          = 0x9244;

全部は調べてませんが、UNPACK_FLIP_Y_WEBGLというのは使ったことがありました。こんなふうに書くと

  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);

テクスチャーのy座標のゼロを下にしてくれるというものです。まあ無いなら無いで計算で反転させてもいいんですけどね。


そんなところです。