天天看點

Vue實作圖形化積木式程式設計(三)前言最終實作效果本文實作效果完整代碼代碼分解後續計劃開源項目GitHub連結資源下載下傳連結你的點贊是我繼續編寫的動力

Babylon.js點選拖拽移動模型

  • 前言
  • 最終實作效果
  • 本文實作效果
  • 完整代碼
  • 代碼分解
    • 0.公用屬性和函數
    • 1.點選落下處理
    • 2.點選移動過程處理
    • 3.點選松開處理
    • 4.綁定滑鼠監聽事件
  • 後續計劃
    • Babylon.js
    • Blockly
  • 開源項目GitHub連結
  • 資源下載下傳連結
  • 你的點贊是我繼續編寫的動力

前言

前段時間想要做一個web端的圖形化積木式程式設計(類似少兒程式設計)的案例,網上沖浪了一圈又一圈,終于技術選型好,然後代碼一頓敲,終于出來了一個雛形。

TIPS:該案例設計主要參考iRobot Coding,隻用做學習用途,侵删。

https://code.irobot.com/#/

最終實作效果

Vue實作圖形化積木式程式設計(三)前言最終實作效果本文實作效果完整代碼代碼分解後續計劃開源項目GitHub連結資源下載下傳連結你的點贊是我繼續編寫的動力

本文實作效果

  • 點選拖拽移動模型
    Vue實作圖形化積木式程式設計(三)前言最終實作效果本文實作效果完整代碼代碼分解後續計劃開源項目GitHub連結資源下載下傳連結你的點贊是我繼續編寫的動力

完整代碼

  • 點選拖拽移動物體
<template>
  <div style="height: 100%;width: 100%;">
    <div>
      <canvas id="renderCanvas"></canvas>
    </div>
  </div>
</template>

<script>
import * as BABYLON from 'babylonjs';
import * as BABYLON_MATERAIAL from "babylonjs-materials"

//全局變量
var scene = null //場景執行個體
var engine = null //3d引擎執行個體
var camera = null //錄影機執行個體
var plane = null //綠地
var ground = null //網格
var skybox = null //天空盒
var car = null //小車
var startingPoint = new BABYLON.Vector3(0, 0, 0)//目前點選位置

async function loadScene() {
  //場景初始化,可看文章一
  scene = initScene()

  //加載網絡模型,可看文章二
  await initRobot()

  //本文内容,監聽拖動事件,實作點選拖動模型
  dragListening()

  //開啟debug視窗
  // scene.debugLayer.show()

}

//滑鼠點選拖動監聽
function dragListening() {
  // 物體拖拽事件
  var canvas = engine.getRenderingCanvas();

  var currentMesh;//目前點選的模型網格

  //判斷目前點選對象是否是地闆
  var getGroundPosition = function () {
    var pickinfo = scene.pick(scene.pointerX, scene.pointerY, function (mesh) {
      return (mesh == ground || mesh == plane);
    });
    if (pickinfo.hit) {
      return pickinfo.pickedPoint;
    }
    return null;
  }

  //滑鼠點下
  var onPointerDown = function (evt) {
    if (evt.button !== 0) {
      return;
    }
    //判斷目前是否點選一個模型網格,如果是地闆、天空盒等對象,則設定hit為false
    var pickInfo = scene.pick(scene.pointerX, scene.pointerY, function (mesh) {
      return (mesh !== ground && mesh !== plane && mesh !== skybox);
    });
    console.log("pickInfo", pickInfo)
    //如果hit為true,則不為地闆、天空盒等對象
    if (pickInfo.hit) {
      currentMesh = pickInfo.pickedMesh;//擷取目前點選對象
      if (currentMesh.parent == null) {
        console.log("no parent")//沒有父節點則就是car對象了
      } else if (currentMesh.parent.name == car.name) {
        //有父節點,證明現在點選的是子對象,而移動需要移動整個小車對象,是以設定目前點選mesh為父節點(即car對象)
        currentMesh = currentMesh.parent
      }
      console.log("currentMesh", currentMesh)
      //擷取目前移動時地闆的坐标
      startingPoint = getGroundPosition(evt);
      //移動物體時,暫時屏蔽相機的移動控制
      if (startingPoint) { // we need to disconnect camera from canvas
        setTimeout(function () {
          camera.detachControl(canvas);
        }, 0);
      }
    }
  }

  //滑鼠點選着移動中
  var onPointerMove = function (evt) {
    if (!startingPoint) {
      return;
    }
    if (!currentMesh) {
      return;
    }
    //更新目前點選的地闆位置
    var current = getGroundPosition(evt);
    if (!current) {
      return;
    }
    //更新目前小車坐标位置為點選的地闆位置
    console.log('startingPoint', startingPoint)
    var diff = current.subtract(startingPoint);
    console.log('diff', diff)
    currentMesh.position.addInPlace(diff);
    console.log("currentMesh.name", currentMesh.name)
    //更新位置資訊
    startingPoint = current;
  }

  //滑鼠點選後松開
  var onPointerUp = function () {
    //恢複相機移動控制
    if (startingPoint) {
      camera.attachControl(canvas, true);
      startingPoint = null;
      return;
    }
  }

  //canvas綁定監聽事件
  canvas.addEventListener("pointerdown", onPointerDown, false);
  canvas.addEventListener("pointerup", onPointerUp, false);
  canvas.addEventListener("pointermove", onPointerMove, false);
}


async function initRobot() {
  console.log('initRobot')
  //模型url路徑
  const url = "http://localhost:8887/"
  //模型名稱
  const modelName = "sportcar.babylon"
  var result = await BABYLON.SceneLoader.ImportMeshAsync(null, url, modelName, scene);
  var meshes = result.meshes
  console.log("meshes", meshes)
  //定義一個根節點
  var parent = new BABYLON.Mesh("car", scene);
  const scale = 10//縮放比例
  for (var mesh of meshes) {
    mesh.scaling = new BABYLON.Vector3(scale, scale, scale)
    mesh.parent = parent
  }
  //将根節點設定為全局變量
  car = parent


}

function initScene() {
  //擷取到renderCanvas這個元素
  var canvas = document.getElementById("renderCanvas");
  //初始化引擎
  engine = new BABYLON.Engine(canvas, true);
  //初始化場景
  var scene = new BABYLON.Scene(engine);
  //注冊一個渲染循環來重複渲染場景
  engine.runRenderLoop(function () {
    scene.render();
  });
  //浏覽器視窗變化時監聽
  window.addEventListener("resize", function () {
    engine.resize();
  });

  //相機初始化
  camera = new BABYLON.ArcRotateCamera("Camera", 0, 0, 5, new BABYLON.Vector3(0, 0, 10), scene);
  camera.setPosition(new BABYLON.Vector3(20, 200, 400));
  //相機角度限制
  camera.upperBetaLimit = 1.5;//最大z軸旋轉角度差不多45度俯瞰
  camera.lowerRadiusLimit = 50;//最小縮小比例
  camera.upperRadiusLimit = 1500;//最大放大比例
  //變焦速度
  camera.wheelPrecision = 1; //電腦滾輪速度 越小靈敏度越高
  camera.pinchPrecision = 20; //手機放大縮小速度 越小靈敏度越高
  scene.activeCamera.panningSensibility = 100;//右鍵平移靈敏度
  // 将相機和畫布關聯
  camera.attachControl(canvas, true);

  //燈光初始化
  var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 10, 0), scene);
  //設定高光顔色
  light.specular = new BABYLON.Color3(0, 0, 0);
  //設定燈光強度
  light.intensity = 1

  // 綠地初始化
  var materialPlane = new BABYLON.StandardMaterial("texturePlane", scene);
  materialPlane.diffuseColor = new BABYLON.Color3(152 / 255.0, 209 / 255.0, 115 / 255.0)
  materialPlane.backFaceCulling = false;
  materialPlane.freeze()
  plane = BABYLON.MeshBuilder.CreateDisc("ground", {radius: 3000}, scene);
  plane.rotation.x = Math.PI / 2;
  plane.material = materialPlane;
  plane.position.y = -0.1;
  plane.freezeWorldMatrix()

  //網格地闆初始化
  const groundSide = 144;
  ground = BABYLON.Mesh.CreateGround("ground", groundSide, groundSide, 1, scene, true);
  var groundMaterial = new BABYLON_MATERAIAL.GridMaterial("grid", scene);
  groundMaterial.mainColor = BABYLON.Color3.White();//底闆顔色
  groundMaterial.alpha = 1;//透明度
  const gridLineGray = 0.95;
  groundMaterial.lineColor = new BABYLON.Color3(gridLineGray, gridLineGray, gridLineGray);
  groundMaterial.backFaceCulling = true; // 可看到背面
  //大網格間距
  groundMaterial.majorUnitFrequency = 16;
  //小網格間距
  groundMaterial.minorUnitVisibility = 0;
  const gridOffset = 8; // 網格偏移量
  groundMaterial.gridOffset = new BABYLON.Vector3(gridOffset, 0, gridOffset);
  groundMaterial.freeze(); // 當機材質,優化渲染速度
  ground.material = groundMaterial
  ground.freezeWorldMatrix()

  //天空盒初始化
  var skyMaterial = new BABYLON_MATERAIAL.SkyMaterial("skyMaterial", scene);
  skyMaterial.inclination = 0
  skyMaterial.backFaceCulling = false;
  skybox = BABYLON.Mesh.CreateBox("skyBox", 5000.0, scene);
  skybox.material = skyMaterial;

  return scene
}


export default {
  name: "test",
  data() {
    return {}
  },
  async mounted() {
    //加載場景
    await loadScene()
  },
}
</script>

<style scoped>
#renderCanvas {
  width: 680px;
  height: 680px;
  touch-action: none;
  z-index: 10000;
  border-radius: 10px;
}
</style>
           

代碼分解

本文要實作的功能比較簡單,分解來看就是

1、監聽點選落下,判斷目前對象是否是可移動對象,如果是則将其設定為目前移動對象,并禁用攝像頭控制

2、監聽點選移動過程,擷取點選移動過程中點選地闆的位置,更新該位置為小車目前位置

3、監聽點選松開,恢複攝像頭控制

0.公用屬性和函數

// 物體拖拽事件
  var canvas = engine.getRenderingCanvas();

  var currentMesh;//目前點選的模型網格

  //判斷目前點選對象是否是地闆
  var getGroundPosition = function () {
    var pickinfo = scene.pick(scene.pointerX, scene.pointerY, function (mesh) {
      return (mesh == ground || mesh == plane);
    });
    if (pickinfo.hit) {
      return pickinfo.pickedPoint;
    }
    return null;
  }
           

1.點選落下處理

//滑鼠點下
  var onPointerDown = function (evt) {
    if (evt.button !== 0) {
      return;
    }
    //判斷目前是否點選一個模型網格,如果是地闆、天空盒等對象,則設定hit為false
    var pickInfo = scene.pick(scene.pointerX, scene.pointerY, function (mesh) {
      return (mesh !== ground && mesh !== plane && mesh !== skybox);
    });

    console.log("pickInfo", pickInfo)

    //如果hit為true,則不為地闆、天空盒等對象
    if (pickInfo.hit) {
      currentMesh = pickInfo.pickedMesh;//擷取目前點選對象
      if (currentMesh.parent == null) {
        console.log("no parent")//沒有父節點則就是car對象了
      } else if (currentMesh.parent.name == car.name) {
        //有父節點,證明現在點選的是子對象,而移動需要移動整個小車對象,是以設定目前點選mesh為父節點(即car對象)
        currentMesh = currentMesh.parent
      }

      console.log("currentMesh", currentMesh)

      //擷取目前移動時地闆的坐标
      startingPoint = getGroundPosition(evt);

      //移動物體時,暫時屏蔽相機的移動控制
      if (startingPoint) { // we need to disconnect camera from canvas
        setTimeout(function () {
          camera.detachControl(canvas);
        }, 0);
      }
    }
  }
           

2.點選移動過程處理

//滑鼠點選着移動中
  var onPointerMove = function (evt) {
    if (!startingPoint) {
      return;
    }
    if (!currentMesh) {
      return;
    }
    //更新目前點選的地闆位置
    var current = getGroundPosition(evt);

    if (!current) {
      return;
    }
    //更新目前小車坐标位置為點選的地闆位置
    console.log('startingPoint', startingPoint)
    var diff = current.subtract(startingPoint);
    console.log('diff', diff)
    currentMesh.position.addInPlace(diff);
    console.log("currentMesh.name", currentMesh.name)
    //更新位置資訊
    startingPoint = current;
  }
           

3.點選松開處理

//滑鼠點選後松開
  var onPointerUp = function () {
    //恢複相機移動控制
    if (startingPoint) {
      camera.attachControl(canvas, true);
      startingPoint = null;
      return;
    }
  }
           

4.綁定滑鼠監聽事件

//canvas綁定監聽事件
  canvas.addEventListener("pointerdown", onPointerDown, false);
  canvas.addEventListener("pointerup", onPointerUp, false);
  canvas.addEventListener("pointermove", onPointerMove, false);
           

後續計劃

Babylon.js

  • 自定義啟動界面
  • 物體重力效果
  • babylonjs-gui 按鈕實作
  • babylonjs+ammojs 碰撞體實作
  • 将3d界面放入可拖動視窗中

Blockly

  • 入門使用blockly
  • 自定義block塊
  • blockly第三方元件使用
  • 接入js-interpreter,步驟運作block塊
  • …(想到啥寫啥)

開源項目GitHub連結

https://github.com/Wenbile/Child-Programming-Web

資源下載下傳連結

  • Vue前端源碼
  • ThinkJS後端源碼

你的點贊是我繼續編寫的動力

繼續閱讀