web开发如何入门元宇宙?Blender探索笔记 | 大帅老猿threejs特训

前言

精彩的世界杯决赛期间,参与了胖达老师基于Three.js&Blender的元宇宙搭建入门实训,趁着年前还有点记忆,来做个笔记。本来想在这篇笔记里面完整记下整个流程,但是篇幅实在太长了,本文暂时以Blender探索为主。

基础环境搭建

Three.js提供的API是可以让我们基于原生JavaScript随便玩的,但是为了让我们能在VSCode环境下有更好的代码提示和热更新,我们可以把Vite和Typescript利用起来(而且Three.js的API命名都比较长,对于我这种中式英语都说不好的人来说,纯手写压力太大)。

package.json部分配置如下:

{
  "name": "vite-dashuailaoyuan",
  "version": "0.0.1",
  "scripts": {
    "start": "vite --host",
    // ...
  },
  "devDependencies": {
    "@types/three": "^0.134.0",
    "autoprefixer": "^10.4.0",
    "prettier": "^2.5.0",
    "sass": "^1.43.5",
    "typescript": "^4.3.2",
    "vite": "^2.6.14"
  },
  "dependencies": {
    "three": "^0.134.0"
  }
}

我本地使用的node版本是v14.18.1,我们可以通过npm/pnpm/yarn任一方式安装依赖。

依赖安装成功后,我们可以在/src目录下新建一个JS文件,比如study.js,引入three.js以便于我们随后可以随意输出。

import * as THREE from 'three';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';

console.log('====================================');
console.log(THREE);
console.log('====================================');
// ...

最后,在根目录的index.html文件中引入study.js

<script type="module" src="/src/study.js"></script>

这个时候,我们通过npm run start来启动项目,在浏览器中打开控制台,可以看到three.js的API被打印了出来。

three.js基础环境搭建

初遇展馆模型

元宇宙到底火没火,能不能火?我作为一个小小的web开发无法预知,但是基于元宇宙延伸的web3D交互营销却是在慢热起来,而这些交互必然少不了场景。那我们就借助Blender这个免费开源跨平台的App来手撸一个展馆模型,后续就可以在three.js中加载使用。

因为Blender推荐使用方便的快捷键来操作,一手键盘一手鼠标一把梭,所以我们的操作过程中就尽可能地熟悉快捷键操作。

清场

打开Blender,新建【常规】项目会默认给我们创建一个立方体box,这个时候我们要清场删掉一切,快捷键A全选视图元素,快捷键X唤起删除。

当然,我们也可以点选右侧面板元素,通过快捷键X进行删除。

Blender开局一个box

创建展馆

1、添加柱体

我们先来创建出场馆主体,使用组件快捷键Shift+A唤起【添加】面板-【网格】-【柱体】,此时面板左下角会有针对我们当前操作项的一个编辑面板,我们可以编辑柱体的半径、深度和顶点,特别需要注意的是【柱体】的顶点数,顶点越多会生成越多的面,会使得曲面越圆滑,但是面数越多加载起来就越耗性能,因此我们要针对需求来合理设置顶点数,比如这里我们可以设置为120就够用了。

Blender添加柱体的顶点数

2、复制面,向内挤出

我们点选【柱体】并使用快捷键Tab来进入【编辑模式】,此时通过快捷键1/2/3对应切换到点/边/面的编辑模式。

  • 点选【柱体】
  • 快捷键Tab进入编辑模式,3进入面编辑模式
  • 点选顶部的面,快捷键I进入【内切面】模式,按住鼠标左键移动来控制内切面的大小(调整场馆墙体的厚度),鼠标点击其他区域或者Enter完成退出模式
  • 快捷键E进入【挤出】模式,鼠标点按坐标Z轴向下移动,直至到底部平面位置,调整好位置后退出【挤出】模式

Blender挤出
3、展馆的大门

为了更逼真,我们需要把封闭的柱体拆出来一个大门来。流程很简单:选中面并删除,缝合顶点。

首先呢,我们需要了解操作面板右上角的线框/实体/材质渲染预览的视图着色方式,切换这三种视图方式可以让我们编辑时更直观选中或预览渲染效果。

  • 切换到线框视图模式
  • 快捷键Tab进入到面编辑模式,通过滑动鼠标滚轮调整我们的视角,框选要删除的面(可以通过按着Shift加选面)
  • 快捷键X进入删除模式,删除

Blender删除面

但是此时我们会发现,删除面以后,两侧的连接处是镂空的,我们需要把面缝合起来。

  • 编辑模式下,快捷键1进入到点编辑模式,选中边缘的4个顶点
  • 右键-从顶点创建边/面

three06.gif

好啦,我们的场馆大体基本完工啦,纯毛坯房啊有木有?这里只是基础的入门笔记,对于Blender而言,掌握常用快捷键就够啦。剩下的,就靠我们的反复click,就可以一点点搭建出更完善的细节,当然这个过程还需要更多的时间和耐心,以及兴趣。

Blender模型

如果你愿意给多一点时间,你能创建出一个自己满意的场景,起码不会比我的差哦。

中文的支持

虽说Blender菜单工具栏的国际化中文支持做的很不错,但是我们要在场景中添加中文文本,还是要稍微费一丢丢功夫。

添加文本编辑

比葫芦画瓢,我们参照创建柱体的方法(组合键Shift+A)来添加一个【文本】,默认文本内容是“Text”,但是我们怎么编辑文本内容呢?

大家还记得快捷键Tab可以快速进入编辑模式么?同样地,我们使用这个快捷键,此时进入的就是文本的编辑模式啦。我们输入123还是abc都可以,但是就是输入不了中文。不知道是因为中文字体包太大,还是因为有这个需求的用户比较少,反正Blender目前(v3.4.0)版本预置的文本字体是不支持中文的。

想使用中文怎么办?那我们就需要自己引入中文字体。

引入中文字体

选中【文本】节点时,在工作区右侧会有一个【物体数据属性】的菜单,点击进入后有【字体】选择。点击对应字体右侧的目录图标,会自动进入系统字体目录,选择自己中意的字体即可。

Blender字体面板
Blender中文字体

输入中文文本

选好满意的字体,高高兴兴地输入了"新年快乐",发现依然输入不进去,这可怎么办呢?

莫慌,我们有一个经典的土方法:复制粘贴。把想要展示的文本输入到其他编辑器甚至搜索框任意可以复制的地方,复制粘贴进去。

Blender新年快乐

文本立体化

但是我的文本节点就是一个面,这还怎么玩?那接下来,就要把文本立体化处理。

再次快捷键Tab退出编辑模式,快捷键G移动文本节点到合适的为止,快捷键R旋转节点到合适的角度。

Tips:

1、移动节点时,如果我们担心节点位置乱了,可以锁定轴向进行移动。譬如我们想让节点沿着X轴移动,依次按下快捷键GX,再拖动就可以。

2、旋转节点的时候,会发现原点不在几何中心,右键-设置原点-【原点->几何中心】。

旋转的时候,也可以通过左侧的【旋转】菜单,通过轴向坐标拖拽旋转,旋转时如果有固定角度,可以配合左下角当前编辑面板输入角度值进行旋转。

Blender旋转

  • 方式一:通过右侧面板【修改器属性】-【添加修改器】-【实体化】添加属性面板,设置【厚度】参数即可。
  • 方式二:通过右侧面部【物体数据属性】-【几何数据】,设置【挤出】参数即可。

Blender字体立体化

OK,到这里,关于Blender建模的常规操作已经基本都包含啦,大家可以继续舞起来啦~

代码笔记

import * as THREE from 'three';
// import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';

import dat from 'dat.gui';
import { Vector3 } from 'three';
const gui = new dat.GUI();
const parameters = {
    cameraY: 2,
    cameraZ: -6
}

let mixer;
let playerMixer;

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

camera.position.set(5, 10, 100);
scene.background = new THREE.Color(0.2, 0.2, 0.2);

const ambientLight = new THREE.AmbientLight(0xffffff, 0.1);
scene.add(ambientLight);

const directionLight = new THREE.DirectionalLight(0xffffff, 0.2);
directionLight.castShadow = true;
scene.add(directionLight);

directionLight.shadow.mapSize.width = 2048;
directionLight.shadow.mapSize.height = 2048;

const shadowDistance = 20;
directionLight.shadow.camera.near = 0.5;
directionLight.shadow.camera.far = 50;
directionLight.shadow.camera.left = -shadowDistance;
directionLight.shadow.camera.right = shadowDistance;
directionLight.shadow.camera.top = shadowDistance;
directionLight.shadow.camera.bottom = -shadowDistance;
directionLight.shadow.bias = -0.0001;
directionLight.position.set (10, 10, 10);
directionLight.lookAt(new THREE.Vector3(0, 0, 0));

let playerMesh;
let pointLight;
let actionIdle, actionWalk;
new GLTFLoader().load('../resources/models/player.glb', (gltf) => {
    console.log(gltf);
    gltf.scene.traverse((child) => {
        child.castShadow = true;
        child.receiveShadow = true;
    })
    playerMesh = gltf.scene;
    scene.add(playerMesh);
    playerMesh.position.set(12, -1, 0);

    playerMesh.rotateY(Math.PI);
    playerMesh.add(camera);
    camera.position.set(0, parameters.cameraY, parameters.cameraZ);
    camera.lookAt(playerMesh.position);
    pointLight = new THREE.PointLight(0xffffff, 0.6);
    pointLight.position.set(0, 2, -1);
    scene.add(pointLight);
    playerMesh.add(pointLight);

    playerMixer = new THREE.AnimationMixer(gltf.scene);
    const clipIdle = new THREE.AnimationUtils.subclip(gltf.animations[0], 'idle', 31, 281);
    actionIdle = playerMixer.clipAction(clipIdle);

    const clipWalk= new THREE.AnimationUtils.subclip(gltf.animations[0], 'walk', 0, 30);
    actionWalk = playerMixer.clipAction(clipWalk);
});


let isChangeToWalk = true;
const playerHalfHeight = new THREE.Vector3(0, 1, 0);
window.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowUp') {
        const curPos = playerMesh.position.clone();
        playerMesh.translateZ(1);
        const frontPos = playerMesh.position.clone();
        playerMesh.translateZ(-1);

        const frontVector3 = frontPos.sub(curPos).normalize();
        const raycasterFront = new THREE.Raycaster(playerMesh.position.clone().add(playerHalfHeight), frontVector3);
        const collisionResultsFrontObjs = raycasterFront.intersectObjects(scene.children);
        console.log(collisionResultsFrontObjs);
        if(collisionResultsFrontObjs && collisionResultsFrontObjs[0].distance > 1) {
            playerMesh.translateZ(1);
        }

        if(collisionResultsFrontObjs && collisionResultsFrontObjs.length === 0) {
            playerMesh.translateZ(1);
        }

        if(isChangeToWalk) {
            crossPlay(actionIdle, actionWalk);
            isChangeToWalk = false;
        }
    }
})

window.addEventListener('keyup', (e) => {
    console.log('keyup', e);
    if (e.key === 'ArrowUp') {
        crossPlay(actionWalk, actionIdle);
        isChangeToWalk = true;
    }
});

let prePos;
window.addEventListener('mousemove', (e) => {
    if(prePos) {
        playerMesh.rotateY(-(e.clientX - prePos) * 0.01);
    }
    prePos = e.clientX;
});

window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);
}, false)

new GLTFLoader().load('../resources/models/zhanguan.glb', (gltf) => {
    scene.add(gltf.scene);
    gltf.scene.traverse((child) => {
        child.castShadow = true;
        child.receiveShadow = true;

        if (child.name === '大帅老猿') {
            const video = document.createElement('video');
            video.src = "./resources/yanhua.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();

            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });

            child.material = videoMaterial;
        }
        if (child.name === '大屏幕01' || child.name === '大屏幕02' || child.name === '操作台屏幕' || child.name === '环形屏幕2') {
            const video = document.createElement('video');
            video.src = "./resources/video01.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();

            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });

            child.material = videoMaterial;
        }
        if (child.name === '环形屏幕') {
            const video = document.createElement('video');
            video.src = "./resources/video02.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();

            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });

            child.material = videoMaterial;
        }
        if (child.name === '柱子屏幕') {
            const video = document.createElement('video');
            video.src = "./resources/yanhua.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();

            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });

            child.material = videoMaterial;
        }
    })

    mixer = new THREE.AnimationMixer(gltf.scene);
    const clips = gltf.animations; // 播放所有动画
    clips.forEach(function (clip) {
        const action = mixer.clipAction(clip);
        action.loop = THREE.LoopOnce;
        // 停在最后一帧
        action.clampWhenFinished = true;
        action.play();
    });

})

function crossPlay(curAction, newAction) {
    curAction.fadeOut(0.3);
    newAction.reset();
    newAction.setEffectiveWeight(1);
    newAction.play();
    newAction.fadeIn(0.3);
}


function animate() {
    requestAnimationFrame(animate);

    renderer.render(scene, camera);

    // controls.update();

    if (mixer) {
        mixer.update(0.02);
    }
    if (playerMixer) {
        playerMixer.update(0.015);
    }
}

animate();

写到最后

不管是three.js还是Blender,内容都太多太多了,展开讲三天三夜都讲不完,何况我只是一个寻求入门的web开发。也基于此,在这篇笔记里,我也就暂时忽略了大家可能会比我还熟悉的three.js部分,只留下我这并不完善的代码,着重记录了我第一次接触Blender时遇到的卡壳的地方。
Emm,写得不好请见谅,我要去继续探索了,争取以后能分享给大家更多入门探索笔记。当然,如果你也感兴趣,那就加入猿创营 (v:dashuailaoyuan),一起交流学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
说明文件 程序名:Web Page Blender(网页搅拌机) 功能:将网页进行搅拌 创意开始:2002/4/25 制作日期:2002/4/25 创作意图:本人刚刚开始学习ASP,但在ASP和HTML之间由于不能进行过多的跳转,因此将 HTML代码转换为ASP代码,如: <;;;table width=";;;750";;; border=";;;0";;; cellspacing=";;;0";;; cellpadding=";;;0";;; align=center>;;; <;;;tr bgcolor=";;;#CBCAD2";;;>;;; <;;;td height=";;;68";;;>;;; <;;;div align=";;;center";;;>;;;<;;;font size=";;;6";;;>;;;<;;;b>;;;<;;;font color=";;;#003399";;;>;;;数 据 查 询<;;;/font>;;;<;;;/b>;;;<;;;/font>;;;<;;;/div>;;; <;;;/td>;;; <;;;/tr>;;; <;;;/table>;;; 以上代码先进行替换,";;;替换为";;; & chr(49) & ";;; 再添加前缀s=s & ";;;和后缀";;; 转换后代码如下: s=s & ";;;<;;;table width=";;; & chr(49) & ";;;750";;; & chr(49) & ";;; border=";;; & chr(49) & ";;;0";;; & chr(49) & ";;; cellspacing=";;; & chr(49) & ";;;0";;; & chr(49) & ";;; cellpadding=";;; & chr(49) & ";;;0";;; & chr(49) & ";;; align=center>;;;";;; s=s & ";;; <;;;tr bgcolor=";;; & chr(49) & ";;;#CBCAD2";;; & chr(49) & ";;;>;;;";;; s=s & ";;; <;;;td height=";;; & chr(49) & ";;;68";;; & chr(49) & ";;;>;;;";;; s=s & ";;; <;;;div align=";;; & chr(49) & ";;;center";;; & chr(49) & ";;;>;;;<;;;font size=";;; & chr(49) & ";;;6";;; & chr(49) & ";;;>;;;<;;;b>;;;<;;;font color=";;; & chr(49) & ";;;#003399";;; & chr(49) & ";;;>;;;数 据 查 询<;;;/font>;;;<;;;/b>;;;<;;;/font>;;;<;;;/div>;;;";;; s=s & ";;; <;;;/td>;;;";;; s=s & ";;; <;;;/tr>;;;";;; s=s & ";;;<;;;/table>;;;";;; 其他说明:当然,以上仅举一例说明; 充分发挥你的想像力,把网页“搅”熟 历史记录: 2002/4/25: 1.0版,实现“格式化”的基本功能 2002/4/26: 1.1版,实现“国际版”功能(语言包在language目录下) 只要你愿意,你可以将他变成任意语言版本了 更改替换字符串列表功能:打开language目录下的语言包文件,编辑200到299之间的字串,OK! 联系作者:ameiemail@chinaren.com 凌丽软件: 《内存清洁机》1.4版 功能:清除内存中的程序 1.使内存清洁机总在最上层: 选择“上层(&T)”; 2.过滤系统的程序: 选择“过滤(&F)”; 3.强制关闭应用程序: 在右边的列表框选择应用程序,点击“关闭程序(&C)”; 4.显示/隐藏应用程序: 如上; 5.将应用程序显示在最上层/恢复: 如上; 6.显示可用内存; 7.显示“******”部分的密码: 用鼠标移到“******”上,在右下脚显示。 8.隐藏桌面: 用隐藏方式把“Program Manager”隐藏; 9.隐藏任务条: 用隐藏方式把“任务条”隐藏(在Win2000下会自动恢复); 10.自动追踪: 用鼠标指向一个窗体,列表自动追踪的该窗体; 历史记录 本程序使用 Object Pascal 编写 使用Delphi Build 5.62编译。 1.0 2001年2月 完成基本功能,根据本人写的VB版本移植而来(在功能上有削减)。 1.01 2002年1月 进一步完善,增加了显示“可用内存”、“上层显示”和查看“密码框内容”。 1.1 2002年4月8日 改善了上层显示的效率,直接使用API; 修改自动刷新的BUG; 1.2 2002年4月13日 增加“托盘”功能; 自动保存配置; 1.3 2002年4月13日 增加在启动菜单加入快捷方式; 增加自动追踪功能。 1.4 2002年4月16 增加自动缩到系统栏功能。 文件分析类(Class): 分析文本所有的邮件地址或URL,搜索智能超过市场的邮件群发软件。 WinRoute 日志分析器(软件): 网管好帮手,1.1版 在一个大的公司,为了管理方便和安全性,往往会使用代理上网,WinRoute 是一个 集成路由和防火墙的代理服务器,使用范围非常广泛,但是为了统计一下所列的各种 数据确不是很方便,为此编制本程序,让大家很好的掌握通过代理服务器上网的情况 ,为公司的管理带来便捷。 1.每日通过代理上网的计算机 2.每个计算机访问了多少网站 3.流量分析 4.网站的欢迎度 5.列出未经允许的计算机 6.使用用户词典,个性化配置

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值