天天看点

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后端源码

你的点赞是我继续编写的动力

继续阅读