前言
前段時間想要做一個web端的圖形化積木式編程(類似少兒編程)的案例,網上沖浪了一圈又一圈,終于技術選型好,然后代碼一頓敲,終于出來了一個雛形。
TIPS:該案例設計主要參考iRobot Coding,只用做學習用途,侵刪。
最終實現效果
最終實現效果
本文實現效果
-
實現碰撞效果
實現碰撞效果
完整代碼
- 實現碰撞效果
<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"
import ammo from "ammo.js";
import utils from "./utils";
//全局變量
var scene = null //場景實例
var engine = null //3d引擎實例
var camera = null //攝像機實例
var plane = null //綠地
var ground = null //網格
var skybox = null //天空盒
var car = null //小車
var cubeParent = null //方塊組
var startingPoint = new BABYLON.Vector3(0, 0, 0)//當前點擊位置
// 質量 、摩擦系數、反彈系數
const bodyMass = 0.5, bodyFriction = 0.5, bodyRestitution = 0.9;
const groundFriction = 0.8, groundRestitution = 0.5;
async function loadScene() {
//場景初始化,可看文章一
scene = initScene()
//加載網絡模型,可看文章二
await initRobot()
//可看文章三,監聽拖動事件,實現點擊拖動模型
dragListening()
//本文內容
// 1、初始化重力碰撞系統
await initAmmo()
// 2、將地面和小車加入碰撞體
addPhysicEffect()
//3、加入碰撞體方塊
initCubes()
//開啟debug窗口
// scene.debugLayer.show()
}
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);
//小車啟用碰撞體
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)
//小車實例
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)
// console.log("m.name",m.name)
m.physicsImpostor = new BABYLON.PhysicsImpostor(m, BABYLON.PhysicsImpostor.BoxImpostor, {mass: 0.1}, scene);
})
// 縮放根對象并將其變成physics impostor
physicsRoot.scaling.scaleInPlace(scaling)
physicsRoot.physicsImpostor = new BABYLON.PhysicsImpostor(physicsRoot, BABYLON.PhysicsImpostor.NoImpostor, {
mass: bodyMass,
friction: bodyFriction,
restitution: bodyRestitution
}, scene);
//轉為碰撞體后,其y軸會偏移,偏移比例根據實際調整
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
//對象事件監聽
let actionManager = new BABYLON.ActionManager(scene);
cube.actionManager = actionManager;
cube2.actionManager = actionManager;
cube3.actionManager = actionManager;
// 方塊鼠標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());
}
}));
//方塊鼠標hover離開取消高亮
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();
}
//創建帶碰撞體的方塊
function createBasicRoundedBox(scene, name, size, 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;
}
//鼠標點擊拖動監聽
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: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)
car = meshes
}
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>
- utils.js - 公用方法封裝
var utils = {
//meshs中根據名稱獲取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;
代碼分解
本文要實現的功能:
1、引入Ammo.js物理引擎
2、將地面和小車加入碰撞體,測試小車和地面的碰撞效果
3、加入方塊碰撞體,測試小車和方塊的碰撞效果
0.NPM安裝Ammo.js物理引擎依賴
npm install kripken/ammo.js
1.初始化物理引擎
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);
});
}
2.地面和小車加入碰撞體
function addPhysicEffect() {
//地面啟用碰撞體
plane.physicsImpostor = new BABYLON.PhysicsImpostor(plane, BABYLON.PhysicsImpostor.BoxImpostor, {
mass: 0,
restitution: groundRestitution,
friction: groundFriction
}, scene);
//小車啟用碰撞體
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)
//小車實例
car = physicsRoot
}
- 其中,小車初始化部分作小改動,不直接實例化,而是先存儲meshs的網格列表
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)
// 直接構造一個car的父節點,然后實例化
// 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
//不直接實例化小車節點,car對象存儲meshes網格列表,在小車引入碰撞體后再實例化
car = meshes
}
3.引入方塊碰撞體
- 為了方便看碰撞效果,引入方塊組
- 初始化方塊組,加入碰撞體和hover高亮
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
//對象事件監聽
let actionManager = new BABYLON.ActionManager(scene);
cube.actionManager = actionManager;
cube2.actionManager = actionManager;
cube3.actionManager = actionManager;
// 方塊鼠標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());
}
}));
//方塊鼠標hover離開取消高亮
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();
}
- 創建方塊碰撞體公共方法
//創建方塊碰撞體公共方法
function createBasicRoundedBox(scene, name, size, 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;
}
后續計劃
Babylon.js
- 自定義啟動界面
- babylonjs-gui 按鈕實現
- babylonjs+ammojs 碰撞體實現
- 將3d界面放入可拖動窗口中
Blockly
- 入門使用blockly
- 自定義block塊
- blockly第三方組件使用
- 接入js-interpreter,步驟運行block塊
- ......(想到啥寫啥)