Vue實(shí)現(xiàn)圖形化積木式編程(八)

前言

前段時(shí)間想要做一個(gè)web端的圖形化積木式編程(類(lèi)似少兒編程)的案例,網(wǎng)上沖浪了一圈又一圈,終于技術(shù)選型好,然后代碼一頓敲岩喷,終于出來(lái)了一個(gè)雛形。

TIPS:該案例設(shè)計(jì)主要參考iRobot Coding,只用做學(xué)習(xí)用途监憎,侵刪纱意。

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

最終實(shí)現(xiàn)效果

最終實(shí)現(xiàn)效果

本文實(shí)現(xiàn)效果

  • 將3d界面放入可拖動(dòng)窗口中


    將3d界面放入可拖動(dòng)窗口中

完整代碼

  • 可移動(dòng)的cavans窗口
<template>
  <div style="height: 100%;width: 100%;">
    <!--窗口關(guān)閉后顯示的開(kāi)啟按鈕-->
    <v-btn
        icon
        color="#505781"
        style="padding: 10px 20px 5px 20px;position: absolute;right: 10px;top: 10px;"
        v-show="!show3Dcanvans"
        @click="show3Dcanvans=!show3Dcanvans"
    >
      <v-icon>mdi-equal-box</v-icon>
    </v-btn>
    <!--可移動(dòng)窗口-->
    <div id="window1" v-window="windowParams" v-show="show3Dcanvans">
      <!--頂欄-->
      <div id="header" style="display: flex;justify-content: space-between;">
        <!--標(biāo)題-->
        <div class="header">仿真
        </div>
        <!--關(guān)閉窗口按鈕-->
        <v-btn
            icon
            color="white"
            style="padding: 10px 20px 5px 20px;"
            @click="show3Dcanvans=!show3Dcanvans"
        >
          <v-icon>mdi-close</v-icon>
        </v-btn>
      </div>
      <!--3d引擎cavans-->
      <div style="z-index: 10001;padding-left: 10px;">
        <canvas id="renderCanvas"></canvas>
      </div>
    </div>

    <div style="display: flex;justify-content: space-around;width: 680px;">
      <div>
        <label>alpha:</label>
        <button type="button" class="btn"
                @click="setCameraPosition('alpha',Math.PI/10)">+
        </button>
        <button type="button" class="btn"
                @click="setCameraPosition('alpha',-Math.PI/10)">-
        </button>
      </div>
      <div>
        <label>beta:</label>
        <button type="button" class="btn"
                @click="setCameraPosition('beta',Math.PI/10)">+
        </button>
        <button type="button" class="btn"
                @click="setCameraPosition('beta',-Math.PI/10)">-
        </button>
      </div>
      <div>
        <label>radius:</label>
        <button type="button" class="btn"
                @click="setCameraPosition('radius',Math.PI)">+
        </button>
        <button type="button" class="btn"
                @click="setCameraPosition('radius',-Math.PI)">-
        </button>
      </div>
    </div>
  </div>
</template>

<script>
import * as BABYLON from 'babylonjs';
import * as BABYLON_MATERAIAL from "babylonjs-materials"
import * as GUI from 'babylonjs-gui';
import ammo from "ammo.js";
import utils from "./utils";


const url = "http://localhost:8088/static/simulator/"

//全局變量
var scene = null //場(chǎng)景實(shí)例
var engine = null //3d引擎實(shí)例
var camera = null //攝像機(jī)實(shí)例
var plane = null //綠地
var ground = null //網(wǎng)格
var skybox = null //天空盒
var car = null //小車(chē)
var cubeParent = null //方塊組
var startingPoint = new BABYLON.Vector3(0, 0, 0)//當(dāng)前點(diǎn)擊位置

//質(zhì)量 、摩擦系數(shù)鲸阔、反彈系數(shù)
const bodyMass = 0.5, bodyFriction = 0.5, bodyRestitution = 0.9;
const groundFriction = 0.8, groundRestitution = 0.5;

let speedSelect = null//顯示速度選擇窗
let buttonClicked = false//按鈕是否被點(diǎn)擊

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

  //可看文章五,自定義啟動(dòng)動(dòng)畫(huà)
  customLoadingUI()

  //加載網(wǎng)絡(luò)模型褐筛,可看文章二
  await initRobot()

  //可看文章三类少,監(jiān)聽(tīng)拖動(dòng)事件,實(shí)現(xiàn)點(diǎn)擊拖動(dòng)模型
  dragListening()

  //可看文章四渔扎,實(shí)現(xiàn)碰撞效果
  // 1硫狞、初始化重力碰撞系統(tǒng)
  await initAmmo()
  // 2、將地面和小車(chē)加入碰撞體
  addPhysicEffect()
  //3、加入碰撞體方塊
  initCubes()

  //可看文章五残吩,關(guān)閉啟動(dòng)動(dòng)畫(huà)
  setTimeout(() => {
    hideLoadingUI()
  }, 1000)

  //可看文章六财忽,相機(jī)控制與相機(jī)動(dòng)畫(huà)
  setTimeout(function () {
    console.log(camera.alpha, camera.beta, camera.radius)
    //攝像機(jī)原位置 1.1383885512243588 1.3642551964995249 50
    //通過(guò)相機(jī)控制輸出獲取期望值,然后填入
    ArcAnimation(-1.5649881922490174, 0, 68.84955592153878)
  }, 1500)

  //可看文章七世剖,babylonjs-gui 按鈕實(shí)現(xiàn)
  initButtons()

}


function initButtons() {
  //在場(chǎng)景中設(shè)置一個(gè)全屏的前景2d界面
  var advancedTexture = GUI.AdvancedDynamicTexture.CreateFullscreenUI("btnsUI", true, scene);

  //初始化重啟按鈕
  var restartBtn = GUI.Button.CreateImageOnlyButton(
      "but",
      url + "restart.png"
  );
  restartBtn.height = "60px";
  restartBtn.width = "60px";
  restartBtn.thickness = 0;//邊框
  restartBtn.horizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT//在全屏的水平排列方位
  restartBtn.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_BOTTOM//在全屏的垂直排列方位
  restartBtn.top = "-20px"http://頂部偏移量
  restartBtn.left = "-20px"http://左側(cè)偏移量
  //按鈕點(diǎn)擊時(shí)間監(jiān)聽(tīng)回調(diào)
  restartBtn.onPointerClickObservable.add(function () {
    console.log("重啟引擎")
  });

  //初始化速度選擇彈窗框(包含了龜速和兔速按鈕的向上彈出框)
  speedSelect = new GUI.Rectangle("speedSelect");
  speedSelect.height = "110px";
  speedSelect.width = "60px";
  speedSelect.horizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_LEFT
  speedSelect.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_BOTTOM
  speedSelect.top = "-90px"
  speedSelect.left = "20px"
  speedSelect.thickness = 3;
  speedSelect.color = "#505781";
  speedSelect.background = "white";
  speedSelect.cornerRadius = 15;
  speedSelect.isVisible = false

  //初始化龜速按鈕
  var slowImg = GUI.Button.CreateImageOnlyButton(
      "slowlyBtn",
      url + "turtle.png"
  );
  slowImg.width = "30px";
  slowImg.height = "30px";
  slowImg.thickness = 0;
  slowImg.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_TOP
  slowImg.top = 15;
  slowImg.onPointerClickObservable.add(function () {
    console.log("slowImg click")
    image.source = url + "turtle.png"
    // robotCotroller.setSpeed(1)
    //后續(xù)通過(guò)robot控制器實(shí)例設(shè)置移動(dòng)速度參數(shù)
    console.log('設(shè)置移動(dòng)速度:1')
    speedSelect.isVisible = false//選擇完定罢,關(guān)閉速度選擇彈窗
  });

  //初始化兔速按鈕
  var fastImg = GUI.Button.CreateImageOnlyButton(
      "fastBtn",
      url + "rabbit.png"
  );
  fastImg.width = "30px";
  fastImg.height = "30px";
  fastImg.thickness = 0;
  fastImg.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_BOTTOM
  fastImg.top = -15;
  fastImg.onPointerClickObservable.add(function () {
    console.log("fastImg click")
    image.source = url + "rabbit.png"
    // robotCotroller.setSpeed(6)
    console.log('設(shè)置移動(dòng)速度:6')
    speedSelect.isVisible = false//選擇完笤虫,關(guān)閉速度選擇彈窗
  });

  speedSelect.addControl(slowImg)
  speedSelect.addControl(fastImg)

  //當(dāng)前選擇速度模式按鈕(點(diǎn)擊會(huì)彈出速度選擇彈窗)
  var speedBtn = new GUI.Button("speedBtn");
  speedBtn.height = "60px";
  speedBtn.width = "60px";
  speedBtn.horizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_LEFT
  speedBtn.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_BOTTOM
  speedBtn.top = "-20px"
  speedBtn.left = "20px"
  speedBtn.thickness = 0;
  speedBtn.onPointerClickObservable.add(function () {
    speedSelect.isVisible = !speedSelect.isVisible
    if (speedSelect.isVisible) {
      buttonClicked = true//設(shè)置速度選擇彈窗彈窗狀態(tài)為true旁瘫,用于彈窗后移動(dòng)模型時(shí)取消彈窗狀態(tài)
    }
  });

  //當(dāng)前選擇速度模式按鈕的邊框
  var speedBtnBorder = new GUI.Ellipse();
  speedBtnBorder.width = "60px"
  speedBtnBorder.height = "60px";
  speedBtnBorder.color = "#505781";
  speedBtnBorder.thickness = 3;
  speedBtnBorder.background = "white";
  //當(dāng)前選擇速度模式按鈕的圖案
  var image = new GUI.Image("currentSpeedBtn", url + "turtle.png");
  image.width = "30px";
  image.height = "30px";
  image.thickness = 0;
  speedBtn.addControl(speedBtnBorder);
  speedBtn.addControl(image);

  //將按鈕添加在全屏的2d前景界面中
  advancedTexture.addControl(restartBtn);
  advancedTexture.addControl(speedBtn);
  advancedTexture.addControl(speedSelect);
}


/**
 * 相機(jī)動(dòng)畫(huà)
 * @param toAlpha  動(dòng)畫(huà)完成時(shí)的alpha
 * @param toBeta  動(dòng)畫(huà)完成時(shí)的beta
 * @param toRadius 動(dòng)畫(huà)完成時(shí)的radius
 * @constructor
 */
function ArcAnimation(toAlpha, toBeta, toRadius) {

  let animCamAlpha = new BABYLON.Animation("animCam",
      "alpha",//需要設(shè)置動(dòng)畫(huà)的屬性名稱
      30,//每秒幀數(shù)
      BABYLON.Animation.ANIMATIONTYPE_FLOAT,//屬性變量類(lèi)型  浮點(diǎn)型
      BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT//動(dòng)畫(huà)循環(huán)模式 保持最終狀態(tài)
  );

  let begin = 0, end = 100

  let keysAlpha = [];//alpha動(dòng)畫(huà)關(guān)鍵幀列表,從0-100%琼蚯,alpha從camera.alpha變化到傳入的toAlpha參數(shù)值
  keysAlpha.push({
    frame: begin,
    value: camera.alpha
  });
  keysAlpha.push({
    frame: end,
    value: toAlpha
  });
  animCamAlpha.setKeys(keysAlpha)//配置動(dòng)畫(huà)關(guān)鍵幀列表到動(dòng)畫(huà)對(duì)象中

  //初始化beta動(dòng)畫(huà)參數(shù)
  let animCamBeta = new BABYLON.Animation("animCam", "beta", 30, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT)
  let keysBeta = []
  keysBeta.push({frame: begin, value: camera.beta})
  keysBeta.push({frame: end, value: toBeta})
  animCamBeta.setKeys(keysBeta)

  //初始化radius動(dòng)畫(huà)參數(shù)
  let animCamRadius = new BABYLON.Animation("animCam", "radius", 30, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT)
  let keysRadius = [];
  keysRadius.push({frame: begin, value: camera.radius})
  keysRadius.push({frame: end, value: toRadius})
  animCamRadius.setKeys(keysRadius)

  //加入相機(jī)動(dòng)畫(huà)列表中
  camera.animations.push(animCamAlpha, animCamBeta, animCamRadius)

  //通過(guò)scene開(kāi)啟camera的動(dòng)畫(huà)列表
  scene.beginAnimation(
      camera,//開(kāi)始動(dòng)畫(huà)列表的對(duì)象
      begin,//動(dòng)畫(huà)開(kāi)始幀
      end,//動(dòng)畫(huà)結(jié)束幀
      false,//動(dòng)畫(huà)是否循環(huán)
      6,//動(dòng)畫(huà)的速度比
      () => {
        console.log('camera')
      }//動(dòng)畫(huà)執(zhí)行完成回調(diào)
  )
}

function hideLoadingUI() {
  engine.hideLoadingUI()
  document.getElementById("customLoadingScreenDiv").remove()
}

function customLoadingUI() {

  BABYLON.DefaultLoadingScreen.prototype.displayLoadingUI = function () {
    this._loadingDiv = document.createElement("div");
    this._loadingDiv.id = "customLoadingScreenDiv";
    this._loadingDiv.style.background = "#505781";
    this._loadingDiv.style.zIndex = "10006"
    this._loadingDiv.style.height = "100%"

    var img = new Image()
    img.src = url + "loading.gif";
    img.style.padding = "15%";
    img.style.paddingTop = "30%";
    this._loadingDiv.appendChild(img);

    this._resizeLoadingUI();
    window.addEventListener("resize", this._resizeLoadingUI);

    //這兩個(gè)樣式修改需要在this._resizeLoadingUI之后酬凳,因?yàn)樵摵瘮?shù)執(zhí)行后會(huì)相對(duì)window窗口定位出cavans的位置,然后設(shè)置loading的位置
    //而我們需要的是將其插入到可移動(dòng)窗口中遭庶,以統(tǒng)一窗口的開(kāi)啟關(guān)閉
    this._loadingDiv.style.left = "10px"
    this._loadingDiv.style.top = "39px"

    // document.body.appendChild(this._loadingDiv);
    // 修改為
    // 獲取當(dāng)前可移動(dòng)窗口元素
    let window1 = document.getElementById('window1')
    let header = document.getElementById('header')
    window1.insertBefore(this._loadingDiv,header)
  };

  engine.displayLoadingUI();
}

async function initAmmo() {
  const Ammo = await ammo();
  console.log("Ammo", Ammo)
  //啟用y方向重力
  scene.enablePhysics(new BABYLON.Vector3(0, -10, 0), new BABYLON.AmmoJSPlugin(true, Ammo));

  scene.onReadyObservable.add(function () {
    console.log(scene.getPhysicsEngine()._physicsPlugin.bjsAMMO.btDefaultCollisionConfiguration());
    console.log(scene.getPhysicsEngine()._physicsPlugin._collisionConfiguration);
    console.log(scene.getPhysicsEngine()._physicsPlugin._dispatcher);
    console.log(scene.getPhysicsEngine()._physicsPlugin._solver);
    console.log(scene.getPhysicsEngine()._physicsPlugin.world);
  });
}

function addPhysicEffect() {
  //地面啟用碰撞體
  plane.physicsImpostor = new BABYLON.PhysicsImpostor(plane, BABYLON.PhysicsImpostor.BoxImpostor, {
    mass: 0,
    restitution: groundRestitution,
    friction: groundFriction
  }, scene);

  //小車(chē)啟用碰撞體
  var robotBody = utils.getMeshFromMeshs(car, "Glass_Plane.006")
  console.log('robotBody', robotBody)

  var robotSize = utils.getMeshSize(robotBody)
  var robotScale = 50
  const robotScalingFactor = robotScale / 10;
  var physicsRoot = makePhysicsObjects(car, scene, robotScalingFactor, robotSize)
  //小車(chē)實(shí)例
  car = physicsRoot
}


function makePhysicsObjects(newMeshes, scene, scaling, size) {
  var physicsRoot = new BABYLON.Mesh("robot", scene);
  // physicsRoot.position.y -= 2
  newMeshes.forEach((m) => {
    if (m.parent == null) {
      physicsRoot.addChild(m)
    }
  })

  // 將所有碰撞體加入physics impostor
  physicsRoot.getChildMeshes().forEach((m) => {
    m.scaling.x = Math.abs(m.scaling.x)
    m.scaling.y = Math.abs(m.scaling.y)
    m.scaling.z = Math.abs(m.scaling.z)
    m.physicsImpostor = new BABYLON.PhysicsImpostor(m, BABYLON.PhysicsImpostor.BoxImpostor, {mass: 0.1}, scene);
  })

  // 縮放根對(duì)象并將其變成physics impostor
  physicsRoot.scaling.scaleInPlace(scaling)

  physicsRoot.physicsImpostor = new BABYLON.PhysicsImpostor(physicsRoot, BABYLON.PhysicsImpostor.NoImpostor, {
    mass: bodyMass,
    friction: bodyFriction,
    restitution: bodyRestitution
  }, scene);

  //轉(zhuǎn)為碰撞體后宁仔,其y軸會(huì)偏移,偏移比例根據(jù)實(shí)際調(diào)整
  const impostorOffset = -(size.y) / 1.1
  physicsRoot.physicsImpostor.setDeltaPosition(new BABYLON.Vector3(0, impostorOffset, 0));
  physicsRoot.position.subtractInPlace(new BABYLON.Vector3(0, -impostorOffset, 0));
  return physicsRoot
}

function initCubes() {
  var scale = 1
  const scalingFactor = scale / 10;
  cubeParent = new BABYLON.TransformNode("cubes");
  const cubeHeight = 80 * scalingFactor
  var cube = createBasicRoundedBox(scene, "cube", cubeHeight)

  cube.position._y += cubeHeight / 2
  cube.position._x -= 100
  cube.material = new BABYLON.StandardMaterial("amaterial", scene);
  cube.material.diffuseColor = new BABYLON.Color3(16 / 255.0, 156 / 255.0, 73 / 255.0);
  cubeParent[0] = cube

  var cube2 = createBasicRoundedBox(scene, "cube2", cubeHeight)
  cube2.position._y += cubeHeight / 2
  cube2.position._x -= 100
  cube2.position._z += cubeHeight * 2
  cube2.material = new BABYLON.StandardMaterial("amaterial", scene);
  cube2.material.diffuseColor = new BABYLON.Color3(48 / 255.0, 102 / 255.0, 150 / 255.0);
  cubeParent[1] = cube2

  var cube3 = createBasicRoundedBox(scene, "cube3", cubeHeight)
  cube3.position._y += cubeHeight / 2
  cube3.position._x -= 100
  cube3.position._z -= cubeHeight * 2
  cube3.material = new BABYLON.StandardMaterial("amaterial", scene);
  cube3.material.diffuseColor = new BABYLON.Color3(199 / 255.0, 88 / 255.0, 93 / 255.0);
  cubeParent[2] = cube3

  //對(duì)象事件監(jiān)聽(tīng)
  let actionManager = new BABYLON.ActionManager(scene);
  cube.actionManager = actionManager;
  cube2.actionManager = actionManager;
  cube3.actionManager = actionManager;

  // 方塊鼠標(biāo)hover高亮
  var hl = new BABYLON.HighlightLayer("hl1", scene);
  actionManager.registerAction(new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnPointerOverTrigger, function (evn) {
    var hover_cube = evn.meshUnderPointer.id
    if (hover_cube == cube.name) {
      hl.addMesh(cube, BABYLON.Color3.White());
    } else if (hover_cube == cube2.name) {
      hl.addMesh(cube2, BABYLON.Color3.White());
    } else if (hover_cube == cube3.name) {
      hl.addMesh(cube3, BABYLON.Color3.White());
    }


  }));
  //方塊鼠標(biāo)hover離開(kāi)取消高亮
  actionManager.registerAction(new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnPointerOutTrigger, function (evn) {
    var hover_cube = evn.meshUnderPointer.id
    if (hover_cube == cube.name) {
      hl.removeMesh(cube);
    } else if (hover_cube == cube2.name) {
      hl.removeMesh(cube2);
    } else if (hover_cube == cube3.name) {
      hl.removeMesh(cube3);
    }
  }));

  scene.freezeMaterials();
}

//創(chuàng)建帶碰撞體的方塊
function createBasicRoundedBox(scene, name, size) {
  let mass = 0.25, restitution = 0.5, friction = 0.5
  const boxSide = size;
  const sphereSide = boxSide * 3.1 / 2;
  const sphere = BABYLON.MeshBuilder.CreateSphere('sphere', {diameter: sphereSide, segments: 16}, scene);
  const box = BABYLON.Mesh.CreateBox('box', boxSide, scene);
  const intersection = BABYLON.CSG.FromMesh(box).intersect(BABYLON.CSG.FromMesh(sphere));
  sphere.dispose();
  box.dispose();
  const roundedBox = intersection.toMesh(
      name,
      new BABYLON.StandardMaterial('roundedBoxMaterial', scene),
      scene
  );
  roundedBox.draggable = true;
  roundedBox.physicsImpostor = new BABYLON.PhysicsImpostor(
      roundedBox,
      BABYLON.PhysicsImpostor.BoxImpostor,
      {mass: mass, restitution: restitution, friction: friction}
  );
  roundedBox.material.freeze();
  roundedBox.material.specularColor = new BABYLON.Color3(0, 0, 0);
  roundedBox.freezeWorldMatrix()
  return roundedBox;
}

//鼠標(biāo)點(diǎn)擊拖動(dòng)監(jiān)聽(tīng)
function dragListening() {
  // 物體拖拽事件
  var canvas = engine.getRenderingCanvas();

  var currentMesh;//當(dāng)前點(diǎn)擊的模型網(wǎng)格

  //判斷當(dāng)前點(diǎn)擊對(duì)象是否是地板
  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;
  }

  //鼠標(biāo)點(diǎn)下
  var onPointerDown = function (evt) {
    if (evt.button !== 0) {
      return;
    }
    //判斷當(dāng)前是否點(diǎn)擊一個(gè)模型網(wǎng)格峦睡,如果是地板翎苫、天空盒等對(duì)象,則設(shè)置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榨了,則不為地板煎谍、天空盒等對(duì)象
    if (pickInfo.hit) {
      currentMesh = pickInfo.pickedMesh;//獲取當(dāng)前點(diǎn)擊對(duì)象
      if (currentMesh.parent == null) {
        console.log("no parent")//沒(méi)有父節(jié)點(diǎn)則就是car對(duì)象了
      } else if (currentMesh.parent.name == car.name) {
        //有父節(jié)點(diǎn),證明現(xiàn)在點(diǎn)擊的是子對(duì)象龙屉,而移動(dòng)需要移動(dòng)整個(gè)小車(chē)對(duì)象呐粘,所以設(shè)置當(dāng)前點(diǎn)擊mesh為父節(jié)點(diǎn)(即car對(duì)象)
        currentMesh = currentMesh.parent
      }
      console.log("currentMesh", currentMesh)
      //獲取當(dāng)前移動(dòng)時(shí)地板的坐標(biāo)
      startingPoint = getGroundPosition(evt);
      //移動(dòng)物體時(shí),暫時(shí)屏蔽相機(jī)的移動(dòng)控制
      if (startingPoint) { // we need to disconnect camera from canvas
        setTimeout(function () {
          camera.detachControl(canvas);
        }, 0);
      }
    }
  }

  //鼠標(biāo)點(diǎn)擊著移動(dòng)中
  var onPointerMove = function (evt) {
    if (!startingPoint) {
      return;
    }
    if (!currentMesh) {
      return;
    }
    //更新當(dāng)前點(diǎn)擊的地板位置
    var current = getGroundPosition(evt);
    if (!current) {
      return;
    }
    //更新當(dāng)前小車(chē)坐標(biāo)位置為點(diǎn)擊的地板位置
    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;
  }

  //鼠標(biāo)點(diǎn)擊后松開(kāi)
  var onPointerUp = function () {
    //如果速度選擇窗口位關(guān)閉转捕,則關(guān)閉窗口
    if (buttonClicked) {
      buttonClicked = false
      speedSelect.isVisible = false
    }


    //恢復(fù)相機(jī)移動(dòng)控制
    if (startingPoint) {
      camera.attachControl(canvas, true);
      startingPoint = null;
      return;
    }
  }

  //canvas綁定監(jiān)聽(tīng)事件
  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:8088/static/model/"
  //模型名稱
  const modelName = "sportcar.babylon"
  var result = await BABYLON.SceneLoader.ImportMeshAsync(null, url, modelName, scene);
  var meshes = result.meshes
  console.log("meshes", meshes)
  //不直接實(shí)例化小車(chē)節(jié)點(diǎn)作岖,car對(duì)象存儲(chǔ)meshes網(wǎng)格列表,在小車(chē)引入碰撞體后再實(shí)例化
  car = meshes
}

function initScene() {
  //獲取到renderCanvas這個(gè)元素
  var canvas = document.getElementById("renderCanvas");
  //初始化引擎
  engine = new BABYLON.Engine(canvas, true);
  //初始化場(chǎng)景
  var scene = new BABYLON.Scene(engine);
  //注冊(cè)一個(gè)渲染循環(huán)來(lái)重復(fù)渲染場(chǎng)景
  engine.runRenderLoop(function () {
    scene.render();
  });
  //瀏覽器窗口變化時(shí)監(jiān)聽(tīng)
  window.addEventListener("resize", function () {
    engine.resize();
  });

  //相機(jī)初始化
  camera = new BABYLON.ArcRotateCamera("Camera", 0, 0, 0, new BABYLON.Vector3(0, 0, 0), scene);
  //這里的值可通過(guò)課程6的相機(jī)控制手動(dòng)控制獲取期望位置
  camera.alpha = 1.1383885512243588
  camera.beta = 1.3642551964995249
  camera.radius = 50
  // (new BABYLON.Vector3(18, 9, 39));
  //相機(jī)角度限制
  camera.upperBetaLimit = 1.5;//最大z軸旋轉(zhuǎn)角度差不多45度俯瞰
  camera.lowerRadiusLimit = 50;//最小縮小比例
  camera.upperRadiusLimit = 1500;//最大放大比例
  //變焦速度
  camera.wheelPrecision = 1; //電腦滾輪速度 越小靈敏度越高
  camera.pinchPrecision = 20; //手機(jī)放大縮小速度 越小靈敏度越高
  scene.activeCamera.panningSensibility = 100;//右鍵平移靈敏度
  // 將相機(jī)和畫(huà)布關(guān)聯(lián)
  camera.attachControl(canvas, true);

  //燈光初始化
  var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 10, 0), scene);
  //設(shè)置高光顏色
  light.specular = new BABYLON.Color3(0, 0, 0);
  //設(shè)置燈光強(qiáng)度
  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()

  //網(wǎng)格地板初始化
  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; // 可看到背面
  //大網(wǎng)格間距
  groundMaterial.majorUnitFrequency = 16;
  //小網(wǎng)格間距
  groundMaterial.minorUnitVisibility = 0;
  const gridOffset = 8; // 網(wǎng)格偏移量
  groundMaterial.gridOffset = new BABYLON.Vector3(gridOffset, 0, gridOffset);
  groundMaterial.freeze(); // 凍結(jié)材質(zhì)五芝,優(yōu)化渲染速度
  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 {
      show3Dcanvans: true,
      //移動(dòng)窗口配置
      windowParams: {
        movable: true,
        resizable: false
      }
    }
  },
  async mounted() {
    //加載場(chǎng)景
    await loadScene()
  },
  methods: {
    setCameraPosition(type, value) {
      console.log(type, value)

      switch (type) {
        case 'alpha':
          camera.alpha += value
          break;
        case 'beta':
          camera.beta += value
          break
        case 'radius':
          camera.radius += value
          break
      }
      let {alpha, beta, radius} = camera
      console.log(`更改后的值:${alpha},${beta},${radius}`)
    }
  }
}
</script>

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

.btn {
  background-color: #D9D9D9;
  padding: 2px 15px;
  margin: 5px;
  border-radius: 4px;
  width: 50px;
}

#window1 {
  background-color: #505781;
  border-radius: 10px;
  width: 700px;
  position: absolute;
  top: 5px;
  right: 55px;
  z-index: 10005;
}


.header {
  padding: 10px 20px 5px 20px;
  color: white;
  display: flex;
}
</style>
  • utils.js - 公用方法封裝
var utils = {
    //meshs中根據(jù)名稱獲取mesh
    getMeshFromMeshs(newMeshes, name) {
        var mesh = null
        newMeshes.forEach(m => {
            if (m.name == name) {
                mesh = m
            }
        })
        return mesh
    },

    //獲取mesh的尺寸信息
    getMeshSize(checkmesh) {
        const sizes = checkmesh.getHierarchyBoundingVectors()
        const size = {
            x: (sizes.max.x - sizes.min.x),
            y: (sizes.max.y - sizes.min.y),
            z: (sizes.max.z - sizes.min.z)
        }
        return size
    },
}
export default utils;

代碼分解

本文要實(shí)現(xiàn)的功能:
1痘儡、vue-directive-window基本使用
2、加入頂欄枢步,設(shè)置可移動(dòng)窗口的開(kāi)啟與關(guān)閉
3谤辜、細(xì)節(jié)優(yōu)化,解決加載窗口定位問(wèn)題

0.vue-directive-window庫(kù)安裝與引入

vue-directive-window

  • 安裝依賴
npm install vue-directive-window --save
  • 引入模塊
//全局引用
//main.js
import VueDirectiveWindow from 'vue-directive-window'
Vue.use(VueDirectiveWindow);

1.vue-directive-window使用

  • vue-directive-window通過(guò)設(shè)置Vue的自定義指令集v-window(directive)來(lái)實(shí)現(xiàn)窗口位置和大小的控制
<template>
  <div v-window="windowParams">
    <!-- 可移動(dòng)窗口的內(nèi)容价捧,這里放babylonjs的cavans -->
  </div>
</template>
<script>

export default {
  data() {
    return {
      //移動(dòng)窗口配置
      windowParams: {
        movable: true,//可拖動(dòng)
        resizable: false//不可改變大小
      }
    };
  },
}
</script>

2.加入頂欄丑念,設(shè)置可移動(dòng)窗口的開(kāi)啟與關(guān)閉

  • 加入頂欄,方便移動(dòng)點(diǎn)擊和關(guān)閉
    <!--窗口關(guān)閉后顯示的開(kāi)啟按鈕-->
    <v-btn
        icon
        color="#505781"
        style="padding: 10px 20px 5px 20px;position: absolute;right: 10px;top: 10px;"
        v-show="!show3Dcanvans"
        @click="show3Dcanvans=!show3Dcanvans"
    >
      <v-icon>mdi-equal-box</v-icon>
    </v-btn>
    <!--可移動(dòng)窗口-->
    <div class="window1" v-window="windowParams" v-show="show3Dcanvans">
      <!--頂欄-->
      <div style="display: flex;justify-content: space-between;">
        <!--標(biāo)題-->
        <div style="padding: 10px 20px 5px 20px;color: white;display: flex;">仿真
        </div>
        <!--關(guān)閉窗口按鈕-->
        <v-btn
            icon
            color="white"
            style="padding: 10px 20px 5px 20px;"
            @click="show3Dcanvans=!show3Dcanvans"
        >
          <v-icon>mdi-close</v-icon>
        </v-btn>
      </div>
      <!--3d引擎cavans-->
      <div style="z-index: 10001;padding-left: 10px;">
        <canvas id="renderCanvas"></canvas>
      </div>
    </div>

3.細(xì)節(jié)優(yōu)化结蟋,解決加載窗口定位問(wèn)題

  • 前文中自定義loading加載界面窗口脯倚,其定位是相對(duì)于window全局窗口的趋惨,所以當(dāng)加載窗口存在的時(shí)候拖動(dòng)可移動(dòng)窗口爬坑,加載窗口保留在原來(lái)位置
  • 解決方案就是將加載窗口加到可移動(dòng)窗口的元素中,然后使加載窗口相對(duì)于可移動(dòng)窗口定位
function customLoadingUI() {

  BABYLON.DefaultLoadingScreen.prototype.displayLoadingUI = function () {
    this._loadingDiv = document.createElement("div");
    this._loadingDiv.id = "customLoadingScreenDiv";
    this._loadingDiv.style.background = "#505781";
    this._loadingDiv.style.zIndex = "10006"
    this._loadingDiv.style.height = "100%"

    var img = new Image()
    img.src = url + "loading.gif";
    img.style.padding = "15%";
    img.style.paddingTop = "30%";
    this._loadingDiv.appendChild(img);

    this._resizeLoadingUI();
    window.addEventListener("resize", this._resizeLoadingUI);

    //這兩個(gè)樣式修改需要在this._resizeLoadingUI之后,因?yàn)樵摵瘮?shù)執(zhí)行后會(huì)相對(duì)window窗口定位出cavans的位置挟憔,然后設(shè)置loading的位置
    //而我們需要的是將其插入到可移動(dòng)窗口中,以統(tǒng)一窗口的開(kāi)啟關(guān)閉
    this._loadingDiv.style.left = "10px"
    this._loadingDiv.style.top = "39px"

    // document.body.appendChild(this._loadingDiv);
    // 修改為
    // 獲取當(dāng)前可移動(dòng)窗口元素
    let window1 = document.getElementById('window1')
    let header = document.getElementById('header')
    window1.insertBefore(this._loadingDiv,header)
  };

  engine.displayLoadingUI();
}

后續(xù)計(jì)劃

Blockly

  • 入門(mén)使用blockly
  • 自定義block塊
  • blockly第三方組件使用
  • 接入js-interpreter幻馁,步驟運(yùn)行block塊
  • ......(想到啥寫(xiě)啥)

開(kāi)源項(xiàng)目GitHub鏈接

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

資源下載鏈接

你的點(diǎn)贊是我繼續(xù)編寫(xiě)的動(dòng)力

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末庇配,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子尊残,更是在濱河造成了極大的恐慌炒瘸,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寝衫,死亡現(xiàn)場(chǎng)離奇詭異顷扩,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)慰毅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)隘截,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人汹胃,你說(shuō)我怎么就攤上這事婶芭。” “怎么了着饥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵犀农,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我贱勃,道長(zhǎng)井赌,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任贵扰,我火速辦了婚禮仇穗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘戚绕。我一直安慰自己纹坐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布舞丛。 她就那樣靜靜地躺著耘子,像睡著了一般。 火紅的嫁衣襯著肌膚如雪球切。 梳的紋絲不亂的頭發(fā)上谷誓,一...
    開(kāi)封第一講書(shū)人閱讀 51,598評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音吨凑,去河邊找鬼捍歪。 笑死户辱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的糙臼。 我是一名探鬼主播庐镐,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼变逃!你這毒婦竟也來(lái)了必逆?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤揽乱,失蹤者是張志新(化名)和其女友劉穎名眉,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體锤窑,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡璧针,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年嚷炉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了渊啰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡申屹,死狀恐怖绘证,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情哗讥,我是刑警寧澤嚷那,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站杆煞,受9級(jí)特大地震影響魏宽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜决乎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一队询、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧构诚,春花似錦蚌斩、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至丑蛤,卻和暖如春叠聋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背受裹。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工碌补, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓脑慧,卻偏偏與公主長(zhǎng)得像魄眉,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子闷袒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容