一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

使用 WebGL 進行 3D 開發(fā),第 3 部分: 添加用戶交互

 pengphie 2016-10-23

在 IBM Bluemix 云平臺上開發(fā)并部署您的下一個應(yīng)用。

開始您的試用

硬件加速的 3D 性能是目前每一個 JavaScript 開發(fā)都關(guān)注的事情,因為桌面和移動瀏覽器中對 WebGL 的支持幾乎無處不在。在此 WebGL 系列第 2 部分,我們使用了以下兩個高級別的 WebGL 庫進行實驗:Three.js 和 SceneJS。您看到了這些庫如何通過簡單的、概念性的純 API 來封裝原始 WebGL 開發(fā)的復(fù)雜性,并實現(xiàn)快速的 3D 開發(fā)。您學(xué)習(xí)了如何:

  • 用 10 行 Three.js 代碼而不是 100 行原始 WebGL 來旋轉(zhuǎn)金字塔
  • 通過 Three.js 類利用面向?qū)ο蟮脑O(shè)計,迅速克隆 3D 對象,并創(chuàng)建可以顯示多個運動中對象的場景
  • 使用 Three.js 學(xué)習(xí)基本的 3D 概念,包括場景圖、網(wǎng)格、素材、燈光和攝像機的放置
  • 創(chuàng)建場景中的輪廓式對象的動畫
  • 通過編程實現(xiàn)一個組合了多種運動的場景 — 對象固有的運動和整組對象的運動
  • 通過運用燈光和陰影效果,創(chuàng)建激動人心的場景
  • 使用照片般逼真的紋理,通過紋理貼圖增加被渲染對象的真實感
  • 使用 Three.js 幾何形狀 API 來生成不規(guī)則形狀的 3D 幾何圖形
  • 處理 3D 對象的偏心旋轉(zhuǎn)
  • 從在線 3D 倉庫/存儲庫中獲得復(fù)雜的、渲染就緒的預(yù)制 3D 網(wǎng)格和紋理,并將它們包含在您的 3D 場景中
  • 通過動畫補間(tweening)功能來規(guī)劃多區(qū)域 3D 空間的飛行可視化,為其劃分階段和編寫代碼,并通過 easing 函數(shù)來增強效果
  • 使用 SceneJS 庫輕松創(chuàng)建復(fù)雜的 3D 場景圖形,并對比 SceneJS 的設(shè)計與 Three.js 庫

立刻創(chuàng)建 3D 應(yīng)用程序和數(shù)據(jù)可視化,將目前所學(xué)的技巧付諸于實踐。此 系列 的最后這一篇文章將介紹 3D 用戶交互的一些概念和技術(shù),然后引導(dǎo)您完成兩個 3D 應(yīng)用程序的開發(fā):一個完整的 3D 井字過三關(guān)游戲,以及一個交互式數(shù)據(jù)可視化用戶界面。請參閱 下載,以獲得樣例代碼。

在本文結(jié)束時,您就會掌握在您即將到來的 Web 開發(fā)項目中應(yīng)用 WebGL 和處理 3D 需求的足夠知識。

3D 場景的用戶交互

在此文章系列中,目前為止已經(jīng)構(gòu)建了包含 3D 動畫對象和移動攝像機(飛過或走過)的場景,但使用的都是用戶不能參與的預(yù)先編程的動作。用戶體驗一直類似于觀看視頻。在實踐中,許多 3D 應(yīng)用程序(以及其他許多游戲和數(shù)據(jù)可視化)需要與用戶進行交互。

利用 Three.js 等高層次的 WebGL 庫來增加用戶交互可能非常簡單。在瀏覽器中加載 imatlight.html。這是 第 2 部分 中提供的燈光和陰影效果示例(matlight.html),添加了攝像頭位置的 3D 用戶控制。圖 1 顯示了由 imatlight.html 渲染的場景。

圖 1. 可與用戶進行交互的 3D 場景(在 OS X 上的 Safari 中)
該圖顯示了可與用戶進行交互的 3D 場景(在 OS X 上的 Safari 中)

開始與場景進行交互。通過使用鼠標(biāo),您可以:

  • 水平或垂直平移場景:單擊并按住鼠標(biāo)右鍵,同時移動鼠標(biāo)。
  • 進入或離開場景,以顯示更多或更少的細(xì)節(jié):單擊并按住鼠標(biāo)滾輪(或中心),同時移動鼠標(biāo)。您也可以通過轉(zhuǎn)動鼠標(biāo)滾輪來實現(xiàn)同樣的效果。
  • 圍繞場景運動,保持與場景的當(dāng)前距離:單擊并按住鼠標(biāo)左鍵,同時移動鼠標(biāo)。

通過這些運動和一些練習(xí),您應(yīng)該能夠有效地、快速地在 3D 渲染的場景中實現(xiàn)導(dǎo)航。

圖 2 顯示了進行一些交互后的 imatlight.html。您將從完全不同的角度查看 圖 1 的場景。

圖 2. 進行用戶交互后的另一個角度的 imatlight.html(在 OS X 上的 Chrome 中)
該圖顯示了進行用戶交互后的另一個角度的 imatlight.html(在 OS X 上的 Chrome 中)

清單 1 顯示了 imatlight.html 的代碼。突出顯示的行是添加到 matlight.html(來自 第 2 部分)的代碼,用于將用戶交互融入到場景中。

清單 1. 與 3D 場景 (imatlight.html) 進行交互
<!doctype html>
<html>
<head>
<title>developerWorks WebGL Three.js Interactive Lights and Shadows Effect Example</title>
  <script src="Three.js" ></script>
  <script src="js/controls/OrbitControls.js"></script>

  <script type="text/javascript">

  function draw3D()  {
    var controls;

    function animate() {
      requestAnimationFrame(animate);

      pyramid1.rotateY(Math.PI/180);
      sphere.rotateY(Math.PI/180);
      cube.rotateY(Math.PI/180);
      multi.rotateY(Math.PI/480);
      renderer.render(scene, camera);
    }
    function updateControls() {
      controls.update();
    }

    var geo = new THREE.CylinderGeometry(0,2,2,4,1, true);
    var pyramid1 = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({color:0xff0000}));
    pyramid1.position.set(-2.5, -1, 0);

    geo = new THREE.SphereGeometry(1, 25, 25);
    var sphere = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({color:0x00ff00}));
    sphere.position.set(2.5, -1, 0);

    geo = new THREE.CubeGeometry(2,2,2);
    var cube = new THREE.Mesh(geo,new THREE.MeshPhongMaterial({color:0x0000ff })   );
    cube.position.set(0, 1, 0);

    var camera = new THREE.PerspectiveCamera(  45, 1024/500,0.1, 100);
    camera.position.z = 10;
    camera.position.y = 1;

    controls = new THREE.OrbitControls( camera );
    controls.addEventListener( 'change', updateControls );

    var multi = new THREE.Object3D();
    pyramid1.castShadow = true; sphere.castShadow = true;
    multi.add(cube);
    multi.add(pyramid1);
    multi.add(sphere);
    multi.position.z = 0;

    geo = new THREE.PlaneGeometry(20, 25);
    var floor = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({color :0xcfcfcf}));
    floor.material.side = THREE.DoubleSide;
    floor.rotation.x = Math.PI/2;
    floor.position.y = -2;
    floor.receiveShadow = true;

    var light = new THREE.DirectionalLight(0xe0e0e0);
    light.position.set(5,2,5).normalize();
    light.castShadow = true;
    light.shadowDarkness = 0.5;
    light.shadowCameraRight = 5;
    light.shadowCameraLeft = -5;
    light.shadowCameraTop = 5;
    light.shadowCameraBottom = -5;
    light.shadowCameraNear = 2;
    light.shadowCameraFar = 100;

    var scene = new THREE.Scene();
    scene.add(floor);
    scene.add(multi);
    scene.add(light);
    scene.add(new THREE.AmbientLight(0x101010));

    var div = document.getElementById("shapecanvas2");

    var renderer = new THREE.WebGLRenderer();
    renderer.setSize(1024,500);
    renderer.setClearColor(0x000000, 1);
    renderer.shadowMapEnabled = true;
    div.appendChild( renderer.domElement );
    animate();

  }

</script>

</head>
<body onload="draw3D();">

  <span id="shapecanvas2" style="border: none;" width="1024" height="500"></span>

  <br/>
  </body>

</html>

面向 Three.js 的 OrbitControls API

可選的 Three.js 控制 API

Three.js 代碼分發(fā)包含用于 3D 硬件和用例的無數(shù)可供選擇的用戶輸入/輸出支持 API,這些 API 都位于源代碼目錄的 examples/js/controls 目錄中。TrackballControls.js 通過一個軌跡球支持用戶交互。FirstPersonControl.js 支持許多第一人稱視角(FPV)游戲玩家所熟悉的用戶交互模式。FlyControl.js 支持飛行模擬器式攝像機控制,支持滾動和俯仰。OculusControls.js 通過 Oculus Rift 支持用戶交互,Oculus Rift 是一個仍在開發(fā)中(并備受期待)的、精密的消費者頭部跟蹤沉浸式虛擬現(xiàn)實設(shè)備。

在 Three.js 中,是通過 OrbitControls.js API 來支持鼠標(biāo)交互的,該 API 位于 Three.js 源代碼樹的 examples/js/controls 目錄。因為不是所有 3D 應(yīng)用程序都要求用來與其他一些硬件設(shè)備進行交互的用戶交互、OrbitControls 和其他 API 是可選的庫(參閱 可選的 Three.js 控制 API 邊欄)。

OrbitControls 的工作原理是在 3D 場景內(nèi)與鼠標(biāo)輸入一致地移動攝像機的位置。下面的兩行代碼舉例說明了該控制,并使用來自 3D 場景的攝像機將其參數(shù)化:

controls = new THREE.OrbitControls( camera );
controls.addEventListener( 'change', updateControls );

OrbitControls change 偵聽器對 updateControls() 函數(shù)進行了回調(diào),該函數(shù)的定義是:

function updateControls() {
  controls.update();
    }

在 imatlight.html 3D 場景中,animate() 回調(diào)函數(shù)已經(jīng)通過一個 rAF 鉤在刷新屏幕時更新對象旋轉(zhuǎn);這就是為什么 updateControls() 只需要調(diào)用 controls.update()。如果被渲染的場景是靜態(tài)的,那么 rAF 不會被鉤住,而且只有在檢測到控制變化時才會進行渲染。在這種情況下,updateControls() 函數(shù)還應(yīng)該調(diào)用渲染器的渲染函數(shù)來更新場景。

回頁首

設(shè)計 3D 游戲

您要開發(fā)的下一個項目是一個需要用戶交互的完全可玩的 3D 游戲:3D 井字過三關(guān)游戲。兩名對手玩家持不同顏色的棋子。一個玩家的棋子是紅色的,另一個玩家的棋子是綠色的。玩家輪流將自己的棋子放在一個 3x3x3 的立方體 “井字框架(cage)” 里面。三個棋子沿任意維度排成一行的第一個玩家將會獲得勝利。這里給出的代碼實現(xiàn)了一個電腦玩家(控制紅色棋子),您可以與其進行對戰(zhàn)。核心 “引擎” 代碼是獨立的,可以修改為人與人對戰(zhàn)的游戲,也許可以通過網(wǎng)絡(luò)實現(xiàn)對戰(zhàn)。

圖 3 顯示了玩這個游戲用的 3D 立方體。您可以看到 3x3x3=27 個位置,棋子可以放在這些位置上,所有位置都由白球占據(jù)。

圖 3. 3D 井字過三關(guān)游戲的比賽場地(在 OS X 上的 Firefox 中)
該圖顯示了 3D 井字過三關(guān)游戲的比賽場地(在 OS X 上的 Firefox 中)

當(dāng)玩家圍繞 “井字框架” 運動,思考下一步操作的時候,鼠標(biāo)經(jīng)過的每個可用位置都將變?yōu)辄S色。然后,玩家可以單擊選定的球,提交一步操作,球體就會變?yōu)樵撏婕业念伾?/p>

屏幕上顯示的文字指出了這是哪一個玩家的操作,并在其中一個玩家獲勝時顯示相關(guān)文字。圖 4 顯示了紅色代表的計算機玩家的獲勝組合,以及最終的屏幕顯示。在獲得勝利后,可單擊屏幕上任意位置來重置游戲。

圖 4. 紅色玩家的勝利(在 OS X 上的 Chrome 中)
該圖顯示了紅色玩家的勝利(在 OS X 上的 Chrome 中)

加載 tictacthreed.html,并自己試著玩一下這個游戲。就像在 imatlight.html 中那樣,您可以使用鼠標(biāo)圍繞游戲的井字框架運動。(兩個頁面都使用了 Three.js OrbitControls API。)計算機玩家并不是特別聰明,您可以很輕松地贏得游戲。在游戲結(jié)束時,單擊任意位置再次啟動一個新游戲。前一次比賽中的失敗方在新游戲中可以先走第一步。

在試玩幾場比賽后,查看一下 tictacthreed.html 的源代碼(參見 下載)。清單 2 中的代碼是 tictacthreed.html 的部分,用于創(chuàng)建放棋子的 3D 游戲井字框架。

清單 2. 創(chuàng)建 3D 游戲井字框架
var base = new THREE.Geometry();
for (var z=-1; z<1; z++ ) {
  base.vertices.push(
    new THREE.Vector3(0, 0 ,z), new THREE.Vector3( 3, 0, z ),
    new THREE.Vector3(0, 1 ,z), new THREE.Vector3( 3, 1, z ),
    new THREE.Vector3(1, 2 ,z), new THREE.Vector3( 1, -1, z ),
    new THREE.Vector3(2, 2 ,z), new THREE.Vector3( 2, -1, z )
  );
}
for (var x=1; x<3; x++ ) {
  base.vertices.push(
    new THREE.Vector3(x, 1 ,1), new THREE.Vector3( x, 1, -2 ),
    new THREE.Vector3(x, 0, 1), new THREE.Vector3( x, 0, -2 )
     );
}
var  cage = new THREE.Line(base, new THREE.LineBasicMaterial(), THREE.LinePieces );
cage.position.set(-1.5,-0.5, 0.5);

共有 12 條相交的直線形成這個井字框架。前四條線在 x-y 平面上。第二組的四條線的結(jié)束坐標(biāo)與第一組的相同,但其 z 分量是 -1,而不是 0。最后四條線包含兩組線條,它們的結(jié)束坐標(biāo)除了 x 值外都一樣。使用 THREE.Line 構(gòu)造函數(shù)創(chuàng)建由線 “段”(彼此沒有連接的線段)組成的 cage。在構(gòu)造函數(shù)后面,轉(zhuǎn)換已完成的 cage,讓它的中心位于原點(0,0,0)上。

生成標(biāo)志著游戲位置的球體

為了生成用于標(biāo)志可用位置的白色球體,tictacthreed.html 使用了清單 3 中所示的迭代代碼。

清單 3. 生成白色球體
var geo = new THREE.SphereGeometry(0.3, 25, 25);
var range = [-1, 0, 1];
var idx = 0;
    range.forEach(function(x) {
    range.forEach(function(y) {
    range.forEach(function(z) {
var tempS = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({color:0xffffff}));
    tempS.ID = idx++;
    tempS.claim = UNCLAIMED;
    pos.push(tempS);
    tempS.position.set(x, y, z);
    scene.add(tempS);
    })
  )

});

計算機玩家的實現(xiàn)

通過 redComputerMove() 函數(shù)實現(xiàn)計算機玩家。每當(dāng)輪到紅色玩家的時候,都會調(diào)用 redComputerMove() 來走一步。這個函數(shù)首先會在 wins 數(shù)組中掃描所有獲勝組合,以確定它是否能夠在下一步中獲勝。如果不能,那么它會重新掃描組合,以確定它是否必須阻止綠色玩家即將出現(xiàn)的勝利(因為綠色已占據(jù)一個獲勝組合中的兩個位置,剩下一個還沒有人占據(jù))。由 countClaim() 輔助函數(shù)協(xié)助完成獲勝組合的掃描。如果 redComputerMove() 不能贏,也不需要進行阻止,那么它會通過遍歷 preferred 位置數(shù)組來確定下一步的行動,或者占據(jù)第一個可用的無人占據(jù)的位置。按照這一策略,計算機會 “合理” 地進行下棋,但無法贏得每一場比賽。當(dāng)然,您可以改進游戲策略。

球體的直徑都是 0.6(半徑為 0.3),因此用戶可以透過井字框架看到它們。清單 3 中的代碼創(chuàng)建了白色球體,并將它們放置到所有 27 個可玩的位置。需要注意的是,雖然所有 27 個球體都使用相同的幾何對象(這些對象被命名為 geo)進行網(wǎng)狀構(gòu)造,但每個球體都有一個單獨的實例 THREE.Material。這是必需的,因為代碼會在后面的操作中單獨更改每個球體的顏色。如果所有的網(wǎng)格實例都引用了同一個素材,那么所有球體都將同時改變顏色。

清單 3 的代碼還建立了一個名為 pos 的數(shù)組,它引用 27 個網(wǎng)格,一個球體引用一個網(wǎng)格。勝者決定算法使用了 pos 數(shù)組來檢查是否有玩家已經(jīng)獲勝(并重置游戲)。計算機玩家的代碼也會大量使用 pos 數(shù)組來確定計算機玩家目前是否正在受到威脅,或者是否應(yīng)該走出攻擊性的一步。

每個球體網(wǎng)格對象都有一個額外的屬性,??名為 claim,該屬性被初始化為 UNCLAIMED。當(dāng)用戶在相關(guān)的可玩位置/球體提交一步操作時,該屬性被更改為 REDGREEN。

圖 5 顯示了由 清單 3 的代碼生成的游戲位置的編號方案。每個數(shù)字代表該游戲位置(球體網(wǎng)格)在所生成的 pos 數(shù)組中的索引。勝者決定算法使用了這些指標(biāo)集來判斷玩家是否獲勝。

圖 5. 游戲位置編號方案
該圖顯示了游戲位置編號方案

決定勝方

游戲中有 49 個可能的獲勝組合,每個組合有三個位置。您可以根據(jù)圖 5 手動枚舉這些組合。

在 tictacthreed.html 中,wins 數(shù)組包含了所有獲勝組合的枚舉。為了判斷某個玩家是否獲勝,checkWin(playerColor) 函數(shù)將會遍歷每個組合,并檢查組合中的所有三個球體是否使用了玩家的顏色。通過檢查組合中的每個球體的 claim 屬性來確定勝方;當(dāng)玩家單擊選中的球體時,該屬性被設(shè)置為該玩家的顏色。清單 4 顯示了 checkWin() 的代碼。

清單 4. checkWin() 函數(shù)
function checkWin(color) {
    var won = false;
    var breakEx = {};
    try {
     wins.forEach( function(wincomb) {
      var count = 0;
      wincomb.forEach( function (idx) {
        if (pos[idx].claim == color)
           count++;
      })
      if (count === 3) {
        won = true;
        throw breakEx;

      }

    })
   } catch (ex) {
    if (ex != breakEx) throw ex;

   }
    return won;

}

在清單 4 突出顯示的代碼中,在確定勝方后,checkWin() 通過拋出一個異常來切斷 forEach() 循環(huán),并將 true 狀態(tài)返回給調(diào)用程序。

使用 2D 鼠標(biāo)在 3D 場景中拾取對象

3D 的另一個重要的用戶交互技術(shù)是對象拾取,即 3D 場景中的對象選擇。在井字過三關(guān)游戲中,輸入設(shè)備是一個 2D 鼠標(biāo)。用戶實際單擊的是在其中渲染 3D 場景的畫布。因為當(dāng)用戶周繞場景運動時,渲染會發(fā)生變化,必須將鼠標(biāo)的 2D 坐標(biāo)動態(tài)地(在鼠標(biāo)單擊時)映射到場景的三維坐標(biāo)空間,以確定哪些對象被選中。

在 2D 圖形中,通過命中測試 來執(zhí)行鼠標(biāo)選擇。對象拾取是 3D 中的一種命中測試形式。Three.js 通過提供一個 projector 輔助程序來簡化對象拾取,它可以從 2D 畫布 (x,y) 點過渡到場景的 3D 世界,同時還會考慮到當(dāng)前攝像機的屬性(攝像機所指的方向和角度等)。

Three.js 也有一個 RayCaster 類,可以將光線投射到 3D 場景中,并確定光線是否與場景中指定的 3D 對象集合相交。

在井字過三關(guān)游戲中,在屏幕更新期間執(zhí)行命中測試。鼠標(biāo)移動事件偵聽程序會將鼠標(biāo)的 x 和 y 坐標(biāo)保存為一個全局變量。

在命中測試中,與 Raycaster 相??交的第一個對象用黃色 (RGB Hex 0xffd700) 突出顯示,告訴用戶該位置是可用的。清單 5 顯示了執(zhí)行這一命中測試的 tictacthreed.html 中的代碼。

清單 5. 命中測試
function updateControls() {

var vector = new THREE.Vector3( mouse.x, mouse.y, 0.5 );
    projector.unprojectVector( vector, camera );
var ray = new THREE.Raycaster( camera.position,
    vector.sub( camera.position ).normalize() );
var hits = ray.intersectObjects( pos );

    if (mouse.clicked)  {
     ...
    }
    else { /* mouse move */
     if ( hits.length > 0 )
     {
        ...
     }
     else
     {
      ...;

     }
   }

}

圖形 sprite

圖形 sprite 是在 2D 圖形動畫中使用的基本元素。它們一般是經(jīng)過優(yōu)化的預(yù)渲染圖形的矩形塊,用于在 2D 加速的硬件平臺上快速顯示和動畫。在 3D 渲染的上下文中,sprite 指含有 2D 繪圖/圖形的矩形平面 2D 幾何圖形,比如在 3D 場景中的 2D 文本標(biāo)簽。

在執(zhí)行命中測試后,hits 變量包含 THREE.RayCaster 在場景內(nèi)發(fā)現(xiàn)相交的球體的列表(來自 pos)。如果列表不為空,那么所返回的數(shù)組中的第一個對象就是第一個與投射光線相交的對象(“最上面的” 球體)。

當(dāng)用戶單擊一個可玩的位置時,在該位置上的球體就會變成玩家的顏色。該球體的 claim 屬性也被更新,以反映玩家所走的這一步。清單 6 顯示 tictacthreed.html 的相關(guān)部分。

清單 6. 更新球體的屬性
function updateControls() {

      var vector = new THREE.Vector3( mouse.x, mouse.y, 0.5 );
       projector.unprojectVector( vector, camera );
      var ray = new THREE.Raycaster( camera.position,
            vector.sub( camera.position ).normalize() );
      var hits = ray.intersectObjects( pos );
      if (mouse.clicked)  {
      if ( hits.length > 0 )
      {

        hits[0].object.material.color.setHex((currentMOVE == RED) ?0xff0000 :0x00ff00);
        hits[0].object.claim = currentMOVE;
        updateWin(currentMOVE);
      
}

      mouse.clicked  = false;
    }
    else {
    ...
    }

在 3D 場景上顯示 2D 屏幕上的狀態(tài)

3D 中的公告板

如果要顯示 3D 場景中的文本標(biāo)簽,而不是疊加它,那么您可以通過 Three.js 使用公告板。公告板 是在 3D 渲染中使用的技術(shù),用于顯示無論攝像頭放置在何處都要一直面對觀眾的標(biāo)志或圖形 sprite。公告板通常用來在 3D 可視化中標(biāo)記場景內(nèi)對象(例如,3D 渲染的人體器官),以確保觀眾能夠在導(dǎo)航場景時讀取標(biāo)簽。

如您所知,所有 3D 場景都被渲染為 HTML5 畫布元素中的 2D 圖像序列。為了對屏幕上的狀態(tài)顯示實現(xiàn)半透明的 2D 文本疊加效果,可以創(chuàng)建一個含半透明文本的 CSS3 樣式的 DOM 元素(如 <div><span>),并將它放置在用來渲染 3D 場景的畫布元素的頂部。

對于游戲的屏幕上的狀態(tài)顯示,tictacthreed.html 使用了 CSS 和 HTML DOM 操作,而且不包含 WebGL 調(diào)用。清單 7 顯示了相關(guān)的代碼。

清單 7. 狀態(tài)顯示代碼
var gamestatus = document.createElement('div');
gamestatus.setAttribute("id", "status");
gamestatus.style.cssText =
  "position:absolute;width:300;height:200;color:rgba(255,255,255,0.3);" +
  "background-color:rgba(0,0,0,0);top:10px;" +
  "left:20px;font:bold 48px arial,sans-serif";
gamestatus.innerHTML = "Move:RED";
div.appendChild(gamestatus);

清單 7 顯示了如何通過編程創(chuàng)建樣式化的 <div>,并將它添加到渲染畫布。id 屬性也被添加到 <div> 元素,以便為它提供一個 status ID。

現(xiàn)在您可以通過修改 <div>innerHTML 屬性隨時更新屏幕上的狀態(tài)。在游戲中用于更新屏幕上的狀態(tài)的典型代碼是:

document.getElementById("status").innerHTML = "new status message";

重置游戲

在確定勝方后,全局 gameWon 變量被設(shè)置為 true。當(dāng)游戲處于這種狀態(tài)時,隨后的鼠標(biāo)單擊被切斷,以便調(diào)用 resetGame() 函數(shù),如清單 8 所示。

清單 8. 重置游戲的代碼
function resetGame() {
    pos.forEach( function(position) {
    position.claim = UNCLAIMED;
    position.material.color.setHex(0xffffff);
    document.getElementById("status").innerHTML =
                 ((currentMOVE == RED) ?"Move:GREEN" :"Move:RED");
    currentMOVE = ((currentMOVE == RED) ?GREEN:RED);

  });
 }
  ...

function updateControls() {
    ...
    if (mouse.clicked)  {
      if (gameWON) {
        resetGame();
        gameWON = false;
        mouse.clicked = false;
        return;
      }
      if ( hits.length > 0 )
      {
       ...

resetGame() 函數(shù)將所有可玩的位置再次修改為 UNCLAIMED,并將所有球體的顏色恢復(fù)為白色。它還會更新屏幕上的顯示,讓游戲的失敗方在新游戲中先走第一步。

現(xiàn)在是時候繼續(xù)下一個示例了,您可以嘗試使用 SceneJS 框架創(chuàng)建一個 WebGL 應(yīng)用程序。

回頁首

創(chuàng)建一個 3D 大數(shù)據(jù)導(dǎo)航用戶界面的原型

用戶在 Web 上或移動設(shè)備上所熟悉的數(shù)據(jù)導(dǎo)航界面大多數(shù)是 2D 的。每個用戶都知道如何使用滾動列表、下拉組合框和切換按鈕等 UI 元素。在過去的幾十年中,圖形用戶界面在這方面的變化不大?,F(xiàn)在,利用無所不在的 3D 的可用性,您可以嘗試設(shè)計以 3D 為中心的新數(shù)據(jù)導(dǎo)航用戶界面,讓手頭的應(yīng)用程序顯得更自然。

下一個示例將會探討跨多個數(shù)據(jù)軸導(dǎo)航大型數(shù)據(jù)源的用例。原型用戶界面支持超過 10,000 個綜合數(shù)據(jù)點的一覽導(dǎo)航 — 每個點代表100 個數(shù)據(jù)中心的超過 100 個月(近 9 年)的每月匯總指標(biāo)。圖 6 顯示了這個原型 sjbdvis.html(參見 下載)的實際應(yīng)用。

圖 6. 導(dǎo)航大數(shù)據(jù)集的 3D UI 原型(在 OSX 上的 Safari 中)
該圖顯示了導(dǎo)航大數(shù)據(jù)集的 3D UI 原型(在 OSX 上的 Safari 中)

在圖 6 中,用戶界面??包括在 x-z 平面上的兩個軸。在一個軸上放置數(shù)據(jù)中心位置的名稱(范圍從 AA 到 AZ、BA 到 BZ、CA 到 CZ、DA 到 DV),總共有 100 個地點。在另一個軸上是綜合指標(biāo)的日期,范圍從 2004 年 1 月至 2012 年 4 月,總共是 100 個月。

快速在超過 10,000 個綜合數(shù)據(jù)點之間導(dǎo)航

為每個(月份,數(shù)據(jù)中心)組合渲染一個 3D 矩形條,共 10,000 條。每個矩形條的值的范圍可以是從 1 到 100。

設(shè)置兩個閾值,用于確定被渲染的矩形條的顏色。除非矩形條的值超過 HighThreshold,否則矩形條被渲染為綠色。若矩形條的值介于 HighThresholdPeakThreshold 之間,那么矩形條被渲染為黃色。當(dāng)它的值高于 PeakThreshold 時,矩形條被渲染為紅色。您可以想像 PeakThreshold 紅色矩形條可能表明一個值得進一步研究的特殊情況。

如果能夠可視化相鄰個月(矩形條)的相對值(高度),那么可以幫助識別數(shù)字報告、2D 圖形或熱圖不可能傳達(dá)的特定趨勢或狀況。如果能夠可視化數(shù)據(jù)中心位置之間的相對差值,也會給分析和調(diào)查提供幫助。

您可以使用鼠標(biāo)在 3D 中圍繞顯示運動(就像用 imatlight.html 實現(xiàn)的那樣),更仔細(xì)地查看數(shù)據(jù)集的所有部分。

如圖 7 所示,當(dāng)鼠標(biāo)移過 sjbdvis.html 中的矩形條時,一個大的疊加屏幕顯示提供了關(guān)于當(dāng)前月份和年份的反饋,以及所選中的數(shù)據(jù)中心的名稱。

圖 7. sjbdvis.html 與屏幕上的反饋顯示(在 Windows 上的 Chrome 中)
該圖顯示了sjbdvis.html 與屏幕上的反饋顯示(在 Windows 上的 Chrome 中)

當(dāng)您找到需要進一步調(diào)查的有問題的數(shù)據(jù)點時,可以單擊它來獲取該月份的每日詳細(xì)報告。(在這個原型中,沒有實際的可用數(shù)據(jù)。)系統(tǒng)會提供一個彈出窗口,其中顯示了月份、年份,以及應(yīng)為其提取該報告的數(shù)據(jù)中心,如圖 8 所示。

圖 8. 單擊 sjbdvis.html 中的數(shù)據(jù)條(在 Windows 上的 Firefox 中)
該圖顯示了單擊 sjbdvis.html 中的數(shù)據(jù)條(在 Windows 上的 Firefox 中)

清單 9 中突出顯示的代碼顯示了如何為原型隨機生成數(shù)據(jù)集(模擬 10,000 個綜合數(shù)據(jù)點)。

清單 9. 生成數(shù)據(jù)集
<!DOCTYPE html>
<html lang="en">
<head>
    <title>developerWorks WebGL SceneJS Big Data Visualization Example</title>
    <meta charset="utf-8">
    <script src="./scenejs.js"></script>
<script>
  var mouse = {x:0, y:0, updated: false, clicked: false};
  var monthLabels =
   ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
  var yearLabels =
   ["2004","2005","2006","2007","2008","2009","2010","2011","2012","2013"];
  var PeakThreshold = 97;
  var HighThreshold = 93;

   var data = [];
   var node2Data = {};

   function initData() {
     for (var dim=0; dim<100; dim++)  {
        var axis = [];
        var datacenter = String.fromCharCode(65 + Math.floor(dim /26)) +
            String.fromCharCode(65 + (dim % 26));
        for (var i=0; i<100; i++) {
            var val = Math.floor(Math.random() * 100 + 1);
            var dpoint = { 
               location: datacenter,
               year: yearLabels[ Math.floor(i /12)],
               month: monthLabels[ i % 12],
               value: val};

            axis.push(dpoint);
        }
        data.push(axis);

     }
   }
...

這些數(shù)據(jù)被創(chuàng)建為一個 JavaScript 對象數(shù)組,其名稱為 data[]。每個數(shù)組元素都具有以下結(jié)構(gòu):

{
   location:datacenter location,
   year:year,
   month:month,
   value:value of metrics
}

在生產(chǎn)中,可以從后端數(shù)據(jù)源中獲取此數(shù)據(jù)。

在 sjbdvis.html 中,屏幕上的顯示信息在您使用 UI 時被動態(tài)更新。該顯示包含疊加在 3D 場景上的半透明 2D 文本,該文本是使用前一個例子中所示的相同技術(shù)創(chuàng)建的。

SceneJS 實現(xiàn)大數(shù)據(jù)驅(qū)動的可視化

sjbdvis.html 是使用 SceneJS 構(gòu)建的。正如您從本系列的 第 2 部分 所了解的那樣,SceneJS 可以渲染非常復(fù)雜的場景,并擅長處理數(shù)據(jù)驅(qū)動的應(yīng)用程序。要被渲染的網(wǎng)格可以在 JSON 兼容的 JavaScript 對象節(jié)點的 SceneJS 場景圖(樹)中定義,然后由 SceneJS 解析和渲染。在對場景圖進行解析后,與每個網(wǎng)格相關(guān)聯(lián)的 ID 都可以用于訪問和操作該網(wǎng)格,操作方式類似于動態(tài) HTML5 編程。SceneJS 的這一方面使得它適用于大數(shù)據(jù)可視化應(yīng)用程序。

“核心” 場景圖 coreScene 是靜態(tài)定義的,包含一個簡單的定向光,從左上方指向被渲染的網(wǎng)格。燈光的 idlights1。此 ID 使得用戶可以直接從代碼訪問節(jié)點。清單 10 顯示了 coreScene 的代碼。

清單 10. coreScene 定義
var coreScene = {
     nodes:[
         {
             id:"light1",
             type:"lights",
             lights:[
                         {
                             mode:"dir",
                             color:{ r:1.0, g:1.0, b:1.0 },
                             diffuse:true,
                             specular:true,
                             dir:{ x:-5, y:-2, z:-5 },
                             space:"view"
                         }
             ]

         }
     ]
};;

以編程方式創(chuàng)建一個包含 10,000 個矩形條的場景

以編程方式在 light1 下創(chuàng)建和插入子節(jié)點,根據(jù)空 coreScene 創(chuàng)建最后包含 10,000 個矩形條的場景。

首先,使用場景的異步 getNode() 方法調(diào)用來定位 light1 節(jié)點。在找到 light1 節(jié)點后,使用其 addNode() 方法,一次創(chuàng)建一個矩形條,并將它作為子節(jié)點添加到 light1。清單 11 顯示用于添加子節(jié)點的代碼。

清單 11. 將矩形條添加到 light1 節(jié)點
scene.getNode("light1", function(light) {
   var zpos = 0;
   data.forEach(function(dim) {
     zpos++;
     var xpos = 0;
     dim.forEach(function(dpoint) {
       var curVal = dpoint["value"];
       var curColor = ((curVal > PeakThreshold) ?{ r:1, g:0, b:0} :
          ((curVal > HighThreshold) ?{r:1, g:1, b:0} :{r:0, g:1, b:0}));
       var nodeName = "n" + zpos + "m" + xpos ;
       node2Data[nodeName] = dpoint;
       light.addNode(
           { type:"name",
             name: nodeName,
             nodes:[
                {
                type:"material",
                color: curColor,
                nodes:[   {
                         type:"translate",
                         y: curVal/10,
                         x: xpos++,
                         z: zpos,
                         nodes:[
                         {
                          type:"prims/box",
                          xSize:1,
                          ySize: curVal/10,
                          zSize:1
                         }
                         ]
                     }
                     ]
                   }
                   ]
                 }
           );
       });  // dim.forEach
   });  // data.forEach
});  // scene.getNode("light1")

清單 11 中突出顯示的代碼顯示了所創(chuàng)建的每個 3D 矩形條的節(jié)點結(jié)構(gòu)。每個矩形條都是一個 SceneJS 的原語多維數(shù)據(jù)集幾何形狀(prims/box)的一個實例,并且一個包裝程序 material 節(jié)點為它提供了顏色。使用一個 translate 節(jié)點將它放到所要求的位置。表 1 總結(jié)了關(guān)鍵字段以及如何確定它們的值。

表 1. 創(chuàng)建每個 3D 矩形條時使用的關(guān)鍵字段
節(jié)點類型字段描述
Namenamename 節(jié)點用于實現(xiàn)每個節(jié)點的 “拾取”(或 3D 選擇)。每個矩形條的惟一名稱是用算法根據(jù) x-z 平面上的矩形條位置來生成的。
Materialcolormaterial 節(jié)點為每個矩形條提供其獨有的顏色。顏色用于渲染通過相關(guān)聯(lián)的數(shù)據(jù)點 dpoint["value"] 的值確定的矩形條。HighThresholdPeakThreshold 用于確定是使用綠色、黃色還是紅色。
Translatex在日期/時間序列中每繪制一個矩形條,x 坐標(biāo)增量 1 個單位。每個矩形條被渲染為 x-z 平面上的 1 個單位的正方形,代表一個月份的平均結(jié)果。
Translatey矩形條的 y 位置通過數(shù)據(jù)點值 dpoint["value"] 來確定。它被除以 10,以縮小范圍。
Translatez每個數(shù)據(jù)系列代表一個不同的數(shù)據(jù)中心,占據(jù)一個不同的 z 位置。
prims/boxySize矩形條的 ySize 位置或高度通過數(shù)據(jù)點值 dpoint["value"] 來確定。它被除以 10,以縮小范圍。

在生成 3D 矩形條的節(jié)點時,該代碼還可以填充 node2data 散列。在對象拾取過程中,如果節(jié)點名稱可用,那么可以使用這個散列快速獲取數(shù)據(jù)點信息。您也可以在 SceneJS 節(jié)點內(nèi)創(chuàng)建一個 data 字段,以包含該數(shù)據(jù)。不過,通過檢索節(jié)點來獲得數(shù)據(jù)需要做的工作并不只是進行散列查找這么簡單。

在 SceneJS 中的攝像機軌道運動

像在 SceneJS 中的其他對象一樣,控制渲染后視圖的攝像機是通過 SceneJS 的場景圖中的節(jié)點來表示的。像 Three.js 一樣,SceneJS 支持使用鼠標(biāo)讓攝像機圍繞場景運動。要實現(xiàn)相機軌道運動,可使用包含在 SceneJS 分發(fā)中的 type:"camera/orbit" 插件替代傳統(tǒng)相機類型。在 sjbdvis.html 中的場景創(chuàng)建代碼封裝 coreScene(突出顯示 — 10,000 個矩形條的場景)與一個軌道運動的攝像機節(jié)點:

scene = SceneJS.createScene({
        canvasId:"shapecanvas2",
        nodes:
          [
            { type:"cameras/orbit",
              look:{ x:80, y:0 },
              yaw:0,
              pitch:-20,
              zoom:200,
              zoomSensitivity:10.0,
              nodes:[coreScene]
             }
          ]
        });

如果軌道運動的攝像機節(jié)點已經(jīng)到位,那么實現(xiàn)旋轉(zhuǎn)和縮放的鼠標(biāo)移動是由 SceneJS 庫處理的。lookyawpitch 字段可用來控制攝像機的初始方向和位置。zoom 字段用于確定視圖最初看起來離被渲染的場景有多遠(yuǎn)。

SceneJS 的對象選取

SceneJS 中已啟用對所渲染對象的命中測試的支持。關(guān)鍵是要將每個可拾取對象包裝在 type:"name" 節(jié)點中。在 sjbdvis.html 中渲染的每個矩形條都被包裝在具有惟一名稱的 type:"name" 節(jié)點中,因此,可以從場景中拾取它。在 sjbdvis.html 中,當(dāng)用戶將光標(biāo)移動到被渲染的 3D 矩形條上面時,可使用對象拾取執(zhí)行屏幕上的狀態(tài)顯示的實時更新。每一個可拾取的 3D 矩形條都有以下包裝:

{ type:"name",
  name:unique node name,
  nodes:[
    ...
  ]
}

使用以下代碼,將當(dāng)前鼠標(biāo)位置捕獲到畫布的 mousemove 事件處理程序:

canvas.addEventListener('mousemove',
 function(event) {
   mouse = {x: event.clientX, y: event.clientY, updated: false, clicked: false};

 },
 false);

在更新全局鼠標(biāo)位置/狀態(tài)變量后,事件處理程序會立即返回,以保持響應(yīng)靈敏度。當(dāng)鼠標(biāo)圍繞矩形條移動時,全局 mouse 變量將會捕獲鼠標(biāo)的位置和狀態(tài)。通過以下代碼,在 SceneJS 場景刷新 ticks 時執(zhí)行對象拾取和相關(guān)的屏幕顯示更新:

scene.on("tick", function() {
   if (!mouse.updated) {
     scene.pick(mouse.x, mouse.y);
     mouse.updated = true;
   }
});

在屏幕更新時,根據(jù)鼠標(biāo)的當(dāng)前保存的位置進行對象拾取。如果用戶的鼠標(biāo)懸停在某個矩形條上(或用戶單擊其中一個矩形條),那么這個特定矩形條對象將由 SceneJS 運行時拾取,并發(fā)出一個 SceneJS pick 事件,執(zhí)行一次回調(diào),以表示該對象被命中:

scene.on("pick",
  function (hit) {
    var name = hit.name;
    var datapoint = node2Data[hit.name];
    document.getElementById("dmonth").innerHTML = datapoint["month"];
    document.getElementById("dyear").innerHTML = datapoint["year"];
    document.getElementById("dcenter").innerHTML = datapoint["location"];
    if (mouse.clicked)  {
      //simulate dive into  detailed data
      alert("Show daily detailed reports for " + datapoint["month"] + ", "
        + datapoint["year"] +  " at data center location "
        + datapoint["location"]);
       mouse.clicked = false;
     }

  });

在 sjbdvis.html 中擴展 SceneJS 的數(shù)據(jù)驅(qū)動的性質(zhì)

使用 SceneJS 以編程方式修改所渲染的幾何形狀(基于數(shù)據(jù)點的值)相對容易一些,這提供了動態(tài)更新顯示統(tǒng)計數(shù)據(jù)的可能性??梢栽O(shè)想以下場景:添加一個 “聯(lián)網(wǎng)的實時數(shù)據(jù)提要”,以推動 sjbdvis.html 的更新,從而創(chuàng)建一個實時監(jiān)控控制臺,管理 100 個數(shù)據(jù)中心的多達(dá) 100 種不同的指標(biāo)。

hit 回調(diào)參數(shù)包含由用戶(在 hit.name 中)選取的 type:"name" 的名稱??梢允褂迷撁Q通過 nod2data 映射查找相關(guān)的數(shù)據(jù)點,數(shù)據(jù)點值被用來更新代表屏幕上的狀態(tài)顯示的 CSS 樣式 <div> DOM 元素的內(nèi)容。如果單擊鼠標(biāo),則會彈出一個警報,顯示需要進一步分析的數(shù)據(jù)點 — 模擬由 UI 生成的數(shù)據(jù)獲取事件。

回頁首

使用其他 3D 輸入設(shè)備

關(guān)于本 系列 的最后一個示例,還有一點需要注意。PC 鼠標(biāo)在與 3D 場景交互時就會真正體現(xiàn)出它的落后之處。為了用 3D 線索補充 2D 鼠標(biāo)輸入數(shù)據(jù),您必須在拖動鼠標(biāo)的同時單擊多個按鈕。理想情況下,通過具有 3D 功能的輸入設(shè)備可以直接輸入 3D 位置信息。

Leap Motion Controller 就是這種設(shè)備。這個被廣泛使用的設(shè)備是一個小型傳感器盒,通過 USB 3 連接到您的 PC 或 Mac。通過使用與設(shè)備的硬件紅外燈及攝像機數(shù)組一起工作的一個軟件驅(qū)動程序檢測 3D 位置和手勢信息。該設(shè)備可以精確地檢測手、手指以及在其檢測區(qū)域范圍內(nèi)的手持工具的位置和運動。

圖 9 顯示了一個 imatlight.html 版本(代碼 下載 中的名稱為 ilpmatlight.html)中的 3D 用戶交互,可將它顯示在大電視屏幕上,經(jīng)過修改,可將該版本與 Leap Motion Controller 結(jié)合使用。通過在 3D 空間中移動一只手,您可以平移、旋轉(zhuǎn)和縮放在 Three.js 場景中的攝像機,從不同的角度觀看場景。

圖 9. 在 3D 中通過 Leap Motion Controller 實現(xiàn)的用戶與場景的交互
該圖顯示了在 3D 中通過 Leap Motion Controller 實現(xiàn)的用戶與場景的交互

Leap Motion Controller 的官方 JavaScript 支持軟件是 leapjs。它包含一個 node.js 服務(wù)器,該服務(wù)器與本機驅(qū)動程序進行通信,并通過 WebSocket 將傳感器信息發(fā)送到本地 JavaScript(瀏覽器)客戶端。除了 leapjs 之外, threeleapcontrols 也是一組與 Leap Motion Controller 和 Three.js 配合使用的攝像機和對象控制。清單 12 顯示了 ilpmatlight.html 中的代碼更改,用于支持 3D 手位置控制輸入。

清單 12. 將 Leap Motion 3D 輸入支持添加到 ilpmatlight.html
<!doctype html>
<html>
<head>
  <title>developerWorks WebGL Three.js 3D Interactive Lights 
     and Shadows Effect Example with Leap Motion Controller</title>
  <script src="Three.js" ></script>
  <script src="leap.min.js"></script>
  <script src="LeapCameraControls.js"></script>
  <script type="text/javascript">
  
 
  function draw3D()  {

     var controls;
    Leap.loop(function(frame) {
      pyramid1.rotateY(Math.PI/180);
      sphere.rotateY(Math.PI/180);
      cube.rotateY(Math.PI/180);
      multi.rotateY(Math.PI/480);
      controls.update(frame);
      renderer.render(scene, camera);
    });
    


    var geo = new THREE.CylinderGeometry(0,2,2,4,1, true);
    var pyramid1 = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({color:0xff0000}));
    pyramid1.position.set(-2.5, -1, 0);

    ...
    var camera = new THREE.PerspectiveCamera(  45, 1024/500,0.1, 100);       
    camera.position.z = 10;
    camera.position.y = 1;

    controls = new THREE.LeapCameraControls(camera); 

    var multi = new THREE.Object3D();
    pyramid1.castShadow = true; sphere.castShadow = true; 
    ...
    var div = document.getElementById("shapecanvas2");      
    var renderer = new THREE.WebGLRenderer();
    renderer.setSize(1024,500);
    renderer.setClearColor(0x000000, 1);
    renderer.shadowMapEnabled = true;
    div.appendChild( renderer.domElement );
  }
  </script>
 </head>
 <body onload="draw3D();">
 <span id="shapecanvas2" style="border: none;" width="1024" height="500"></span>
 <br/>
  </body>
</html>

回頁首

主流 3D 計算的曙光

有整整一代計算機用戶從小就開始玩 3D 游戲,他們正準(zhǔn)備加入計算主流。他們渴望得到 3D 相關(guān)技術(shù)帶來的新穎的、創(chuàng)新的應(yīng)用程序,以提高生產(chǎn)率并改進他們的日常計算體驗。在這十年中,大量研究資金傾注到了大數(shù)據(jù)可視化技術(shù)、3D 掃描和 3D 打印中 — 也許這是歷史上第一次讓 3D 中的計算合法化。結(jié)合這種趨勢與當(dāng)今移動設(shè)備不斷增強的 3D 硬件渲染能力(以及能夠使用 WebGL 簡單而有效地駕馭這種能力),JavaScript 開發(fā)人員現(xiàn)在正處于一個令人興奮的浪潮的最前沿。

回頁首

下載

描述名字大小
樣例代碼webGL3dl.zip339KB

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    色婷婷久久五月中文字幕| 99热九九在线中文字幕| 无套内射美女视频免费在线观看| 日韩专区欧美中文字幕| 色欧美一区二区三区在线| 欧美一区二区三区99| 精品推荐久久久国产av| 亚洲精品中文字幕在线视频| 亚洲做性视频在线播放| 亚洲一区二区三区中文久久| 国产女优视频一区二区| 日本一区二区三区黄色| 国产日本欧美特黄在线观看| 成人午夜在线视频观看| 日韩aa一区二区三区| 国产精品亚洲欧美一区麻豆| 久久精品免费视看国产成人| 中文字幕免费观看亚洲视频 | 91一区国产中文字幕| 国产成人精品资源在线观看| 东京热男人的天堂一二三区| 亚洲精品欧美精品一区三区| 在线免费国产一区二区| 美女被后入视频在线观看| 日本精品最新字幕视频播放| 日韩人妻免费视频一专区| 九九热国产这里只有精品| 麻豆蜜桃星空传媒在线观看| 亚洲av首页免费在线观看| 夫妻性生活动态图视频| 我要看日本黄色小视频| 日韩一区二区三区久久| 亚洲综合色在线视频香蕉视频| 免费在线成人激情视频| 国产亚州欧美一区二区| 欧美日韩久久精品一区二区| 黄片免费观看一区二区| 天海翼高清二区三区在线| 国产精品福利精品福利| 日本一二三区不卡免费| 韩国日本欧美国产三级 |