哇咔咔,定制一套3D龙年贺岁饼干

介绍

在龙年春节即将来临之际,本期将制作一套3D龙年图案的饼干物理效果,你可以学习或者了解到,怎么用blender制作几个龙年图案的贺岁饼干,以及用three.js将他们渲染出来,并结合cannon-es给予物理特性,点击可视区域可以生成出随机图案的饼干落下。

演示

jsmask.gitee.io/dragon-year...

正文

饼干模型

这里我们可以从一些模型网站下载合适的饼干模型去调整或者自己建立圆柱调整几个顶点展UV烘焙出来也行,我就是在sketchfab找了一个低模改了些顶点坐标使用的尽可能的处理干净些,如下图:

接下来,要给这个饼干做上龙年图案,所以我们在网上找几个合眼的png图片,我这里找了6张可爱的小龙贺年的图案。

然后,最关键的一步是如何把这些图案融合到已经做好的uv纹理贴图上。这里有三个方案可供选择,第一个是PS软件把图案叠加到纹理图上。第二方案是再创建一个uv图,展开把相应位置贴上去,然后两个uv图叠加。第三个方案,也是我用到的方案,就是建立一个笔刷把笔刷的纹理设置成该图案用镂空模式,设置成正片叠底最后确定好位置刷上去。

渲染模型

我们把做好的模型都导出来,这里我导出一张白模,和6张纹理图,后面我会渲染这个模型后,把纹理图贴上生成不同图案的饼干。至于基础的场景搭建,可以看我以往的文章或者参考源码,这里就不过多赘述了,只讲关键的部分。

首先,我们先获取到模型和它需要的纹理贴图,并下载下来,注意此次开发用的是vite,所以一次性获取多张纹理图获取用到了import.meta.glob,把文件夹下所有符合的图片都拿到,然后用THREE.TextureLoader 加载生成出纹理来,并且我们要把它存到 textures 数组中,以备后续使用,至于,glb模型就很简单用 GLTFLoader 加载处理就好,当然需要稍微处理下它的大小位置角度是否产生阴影或接收阴影等,本次只有两个glb模型分别是饼干和桌子(充当平面)。

js 复制代码
const images = import.meta.glob(`./assets/textures/*.png`, { eager: true });
const imageUrls = Object.values(images).map((mod) => mod.default);
import ck from "./assets/cookie.glb?url" // 饼干模型
import tb from "./assets/kitchen_table.glb?url" // 桌子模型

class Sketch {
    constructor() {
        this.loadingManager = new THREE.LoadingManager();
        this.textureLoader = new THREE.TextureLoader(this.loadingManager);
        this.gltfLoader = new GLTFLoader(this.loadingManager);

        this.cookiesItems = []
        this.textures = []

        this.init();
        return this;
   }
   init() {
    this.loadTexture();
    this.loadCookieModel();
    this.addPlane()

    this.loadingManager.onProgress = ((url, num, total) => {})

    this.loadingManager.onLoad = () => {
       this.play();
    }
   }
   loadTexture() {
        imageUrls.forEach(url=>{
            this.textures.push(this.textureLoader.load(url))
        })
   }
   loadCookieModel() {
        this.gltfLoader.load(ck, (gltf) => {
          this.model = gltf.scene;
          this.model.children[0].position.y -= .01
          this.model.children[0].scale.setScalar(.08)
          this.model.rotation.y = 0
          this.model.traverse(mesh => {
            if (mesh.isMesh) {
              mesh.receiveShadow = true;
              mesh.castShadow = true;
            }
          })
        })
   }
  addPlane() {
    this.gltfLoader.load(tb, (gltf) => {
      let table = gltf.scene;
      table.scale.setScalar(.1)
      table.rotation.y = Math.PI / 2
      table.position.y = -0.912
      table.traverse(mesh => {
        if (mesh.isMesh) {
          mesh.receiveShadow = true;
        }
      })
      this.scene.add(table)
    })
  } 
}

然后,我们可以用给模型附上我们之前做好的纹理材质贴图,图案是随机给出的,之后我们执行这段代码把饼干添加到场景中,可以看到已经可以渲染出来了。

js 复制代码
createCookies(position) {
    let cookies = this.model.clone();
    let material = cookies.getObjectByName("Object_2").material.clone()
    material.map = this.textures[Math.floor(Math.random()*this.textures.length)]
    material.map.flipY = false;
    material.map.encoding = THREE.sRGBEncoding
    material.map.wrapS = 1000
    material.map.wrapT = 1000
    material.needsUpdate = true;
    cookies.getObjectByName("Object_2").material = material
    cookies.position.set(0, .15, 0)
    this.scene.add(cookies);
}

添加物理

这里物理库我使用了比较简单易上手的 cannon-es ,而且物理调试器引入跟它配套的cannon-es-debugger,可以非常方便的看到物理世界的情况,另外,值得说的是,如果把mass属性代表质量,如果设置成0则表示无限大,我们做桌面的时候就要设置它为0,这样就变成一个静态的刚体了。

js 复制代码
import * as CANNON from "cannon-es"
import CannonDebugger from 'cannon-es-debugger'

initCannon() {
    this.world = new CANNON.World({
        gravity: new CANNON.Vec3(0, -9.82 * .18, 0), // m/s²
    })

    this.world.allowSleep = true

    let groundShape = new CANNON.Plane();
    let groundBody = new CANNON.Body({ mass: 0 });
    groundBody.addShape(groundShape);
    groundBody.position.y = -0.8
    groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
    this.world.addBody(groundBody);

    let tableBody = new CANNON.Body({
        mass: 0,
    });
    const tableShape = new CANNON.Box(new CANNON.Vec3(.88, .01, .58))
    tableBody.addShape(tableShape);
    tableBody.position.set(-.07, 0, 0);
    tableBody.type = CANNON.Body.STATIC;
    this.world.addBody(tableBody)

    this.cannonDebugger = CannonDebugger(this.scene, this.world, {
        onInit(body, mesh) {
            mesh.visible = true
        },
    })
}

接下来,我们要改造下createCookies方法,其实创建时也能生成对应的物理形状,再用cookiesItems 数组存储起来,每次update的时候也要执行下,使其模型与刚体状态同步。

js 复制代码
createCookies(position) {
    let cookies = this.model.clone();
    let material = cookies.getObjectByName("Object_2").material.clone()
    material.map = this.textures[Math.floor(Math.random()*this.textures.length)]
    material.map.flipY = false;
    material.map.encoding = THREE.sRGBEncoding
    material.map.wrapS = 1000
    material.map.wrapT = 1000
    material.needsUpdate = true;
    cookies.getObjectByName("Object_2").material = material
    cookies.position.set(0, .15, 0)
    let body = new CANNON.Body({
      mass: 0.2,
      material: new CANNON.Material({
        friction: .15,
        restitution: .01,
      })
    });
    const boxShape = new CANNON.Cylinder(.08, .08, .026, 32)
    body.addShape(boxShape);
    body.position.set(0, 0.2, 0);
    if (position) {
      body.position.copy(position)
    }
    this.cookiesItems.push({
      cookies,
      body
    })
    this.world.addBody(body);
    this.scene.add(cookies);
}
updatePhysics(dt) {
    this.world.fixedStep()
    this.cannonDebugger.update()
    if (this.cookiesItems.length) {
        this.cookiesItems.forEach(({ cookies, body }) => {
            cookies.position.copy(body.position)
            cookies.quaternion.copy(body.quaternion)
        })
    }
}

物理交互

接下来,我们要鼠标点击并抬起后会发出一道射线,如果射线捕获的桌面那么就会在这个点位生成一个随机图案的饼干,从空中落下,执行自由落体运动。

js 复制代码
mouseEffects() {
    this.handlePointerDown = this.pointerDown.bind(this)
    this.handlePointerMove = this.pointerMove.bind(this);
    this.handlePointerUp = this.pointerUp.bind(this);
    this.renderer?.domElement.addEventListener("pointerdown", this.handlePointerDown, false);
    this.renderer?.domElement.addEventListener("pointermove", this.handlePointerMove, false);
    this.renderer?.domElement.addEventListener("pointerup", this.handlePointerUp, false);
}

pointerDown(event) {
    this.changeControls = false;
}
pointerMove(event) {}
pointerUp(event) {
    if (this.changeControls) return;
    this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    this.raycaster.setFromCamera(this.mouse, this.camera);
    const intersects = this.raycaster.intersectObjects(this.scene.children);

    if (intersects.length > 0) {
        const point = intersects[0].point;
        this.createCookies(new THREE.Vector3(point.x, 1, point.z))
    }
}

没有声音可没意思,最后一步,要让饼干在碰撞的瞬间发出声音来,所以其刚体要加碰撞监听事件,当碰撞的力度越大声音也越大。

js 复制代码
createCookies(position) {
    // ...
	body.addEventListener('collide', playHitSound)
}
js 复制代码
import hit from "./assets/hit.mp3?url"
const hitSound = new Audio(hit)
const playHitSound = (collision) => {
  const impactStrength = collision.contact.getImpactVelocityAlongNormal()
  if (impactStrength > 1) {
    hitSound.volume = Math.max(0, Math.min(impactStrength * .25, 1));
    hitSound.currentTime = 0
    hitSound.play()
  }
}

源码

gitee.com/jsmask/drag...

结语

本期文章希望会给各位小伙伴带来收获或者帮助,里面其实还可以做很多好玩的事情,喜欢的小伙伴可以多多点赞收藏哦,最后祝龙年大吉,健康快乐~

相关推荐
王哲晓1 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4114 分钟前
无网络安装ionic和运行
前端·npm
理想不理想v6 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云16 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:1379712058718 分钟前
web端手机录音
前端
齐 飞23 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
神仙别闹40 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子2 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0012 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html