threejs 多屏互动效果,居然还能这么玩

threejs 多屏互动效果

看别人做了多屏互动的效果,觉得还挺有意思的,也顺便自己动手操作一下试试。 先来张效果图:

项目地址

项目基于vue+threejs。

思路

大体思路如下:

  1. 架设一个正投影摄像机,在屏幕中间划一个球。
  2. 显示球的网格并让球转起来。
  3. 转动的时候发送当前球的ID、旋转角度,窗口距离屏幕偏移,窗口大小,时间戳等信息。
  4. 接收到其他窗口的信息,根据计算相对于当前球的位置在屏幕上画其他球。

其中跨浏览器标签页通信用的是 BroadcastChannel API。

实践

下边就开始动手做起来。

环境安装

threejs安装命令如下:

bash 复制代码
npm install --save three

其他部分

架设一个正投影摄像机,在屏幕中间划一个球。

javascript 复制代码
import * as THREE from 'three';
import { onMounted, onUnmounted } from 'vue';

const scene = new THREE.Scene();
var camera = new THREE.OrthographicCamera(-window.innerWidth/2, window.innerWidth/2, window.innerHeight/2, -window.innerHeight/2, -1000, 1000);

var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);

const geometry = new THREE.SphereGeometry( 100, 16, 16 ); 
// wireframe = true 的时候显示网格
const material = new THREE.MeshBasicMaterial( { color: 0xffff00, wireframe: true } ); 
const sphere = new THREE.Mesh( geometry, material );
scene.add(sphere)

var balls = {}
var ballsInfo = {}

function animate() {
    requestAnimationFrame(animate);

    // 让球动起来
    sphere.rotation.x += 0.01;
    sphere.rotation.y += 0.005;

    renderer.render(scene, camera);
}
animate();

onMounted(() => {
  let canvas = document.getElementById('can');
  renderer = new THREE.WebGLRenderer({canvas: canvas});

  resize()
});

window.addEventListener('resize', resize);

function resize() {
  renderer.setSize(window.innerWidth, window.innerHeight);

  camera = new THREE.OrthographicCamera(-window.innerWidth/2, window.innerWidth/2, window.innerHeight/2, -window.innerHeight/2, -1000, 1000);
}

</script>

<template>
  <div id="content">
    <canvas id="can"></canvas>
  </div>
</template>

<style scoped>
#content {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
#can {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
</style>

动的时候发送当前球的信息。

javascript 复制代码
const ballId = `ball-${(new Date()).getTime()}`
const channel = new BroadcastChannel('ball')
channel.postMessage({
  type: 'ball',
  id: ballId,
  rotation: {
    x: sphere.rotation.x,
    y: sphere.rotation.y,
    z: sphere.rotation.z,
  },
  offset: {
    x: window.screenX,
    y: window.screenY,
  },
  size: {
    width: window.innerWidth,
    height: window.innerHeight
  },
  timestamp: (new Date()).getTime()
})

监听其他窗口信息:

javascript 复制代码
channel.addEventListener('message', function(e) {
  // console.log('message is ', e.data)
  if (e.data.id in balls) {

  } else {
    const bgeo = new THREE.SphereGeometry( 100, 16, 16 ); 
    const bmaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } ); 
    const ball = new THREE.Mesh( bgeo, bmaterial )
    balls[e.data.id] = ball;
    ball.name = e.data.id
    scene.add(balls[e.data.id])
  }

  ballsInfo[e.data.id] = e.data
})

并把其他球画在当前页面上。

javascript 复制代码
let now = (new Date()).getTime()

for (var i in balls) {
  if (ballsInfo[i].id == ballId) {
    continue
  }
  if (now - ballsInfo[i].timestamp > 100) {
    let ball = scene.getObjectByName(ballsInfo[i].id)
    scene.remove(ball)
    delete balls[i]
    delete ballsInfo[i]
    continue
  }
  balls[i].position.x = ( (ballsInfo[i].offset.x + ballsInfo[i].size.width / 2) - (window.screenX + window.innerWidth / 2 ) )
  balls[i].position.y = ( (window.screenY + window.innerHeight / 2) - (ballsInfo[i].offset.y + ballsInfo[i].size.height / 2) ) 
  balls[i].position.z = 0
  balls[i].rotation.x = ballsInfo[i].rotation.x;
  balls[i].rotation.y = ballsInfo[i].rotation.y;
  balls[i].rotation.z = ballsInfo[i].rotation.z;
}

其中球的位置其实是有敞口相对于屏幕的偏移加上窗口的一半大小来确定。 这样所有的球的位置就都在同一个屏幕坐标系下了。 之后简单的加减就能确定球的相对位置了。

完整代码

javascript 复制代码
import * as THREE from 'three';
import { onMounted, onUnmounted } from 'vue';

const scene = new THREE.Scene();
var camera = new THREE.OrthographicCamera(-window.innerWidth/2, window.innerWidth/2, window.innerHeight/2, -window.innerHeight/2, -1000, 1000);

var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);

const geometry = new THREE.SphereGeometry( 100, 16, 16 ); 
const material = new THREE.MeshBasicMaterial( { color: 0xffff00, wireframe: true } ); 
const sphere = new THREE.Mesh( geometry, material );
scene.add(sphere)

const ballId = `ball-${(new Date()).getTime()}`

var balls = {}
var ballsInfo = {}

const channel = new BroadcastChannel('ball')
channel.addEventListener('message', function(e) {
  // console.log('message is ', e.data)
  if (e.data.id in balls) {

  } else {
    const bgeo = new THREE.SphereGeometry( 100, 16, 16 ); 
    const bmaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } ); 
    const ball = new THREE.Mesh( bgeo, bmaterial )
    balls[e.data.id] = ball;
    ball.name = e.data.id
    scene.add(balls[e.data.id])
  }

  ballsInfo[e.data.id] = e.data
})

function animate() {
    requestAnimationFrame(animate);

    sphere.rotation.x += 0.01;
    sphere.rotation.y += 0.005;

    channel.postMessage({
      type: 'ball',
      id: ballId,
      rotation: {
        x: sphere.rotation.x,
        y: sphere.rotation.y,
        z: sphere.rotation.z,
      },
      offset: {
        x: window.screenX,
        y: window.screenY,
      },
      size: {
        width: window.innerWidth,
        height: window.innerHeight
      },
      timestamp: (new Date()).getTime()
    })

    let now = (new Date()).getTime()

    for (var i in balls) {
      if (ballsInfo[i].id == ballId) {
        continue
      }
      if (now - ballsInfo[i].timestamp > 100) {
        let ball = scene.getObjectByName(ballsInfo[i].id)
        scene.remove(ball)
        delete balls[i]
        delete ballsInfo[i]
        continue
      }
      balls[i].position.x = ( (ballsInfo[i].offset.x + ballsInfo[i].size.width / 2) - (window.screenX + window.innerWidth / 2 ) )
      balls[i].position.y = ( (window.screenY + window.innerHeight / 2) - (ballsInfo[i].offset.y + ballsInfo[i].size.height / 2) ) 
      balls[i].position.z = 0
      balls[i].rotation.x = ballsInfo[i].rotation.x;
      balls[i].rotation.y = ballsInfo[i].rotation.y;
      balls[i].rotation.z = ballsInfo[i].rotation.z;
    }

    renderer.render(scene, camera);
}
animate();

onMounted(() => {
  let canvas = document.getElementById('can');
  renderer = new THREE.WebGLRenderer({canvas: canvas});

  resize()
});

window.addEventListener('resize', resize);

function resize() {
  renderer.setSize(window.innerWidth, window.innerHeight);

  camera = new THREE.OrthographicCamera(-window.innerWidth/2, window.innerWidth/2, window.innerHeight/2, -window.innerHeight/2, -1000, 1000);
}

</script>

<template>
  <div id="content">
    <canvas id="can"></canvas>
  </div>
</template>

<style scoped>
#content {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
#can {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
</style>
相关推荐
郝学胜-神的一滴1 小时前
中级OpenGL教程 008:精准控制高光光斑大小与强度
c++·unity·godot·three.js·图形学·opengl·unreal
xier1234563 天前
three-instance-batch 开发笔记
javascript·three.js
一根数据线5 天前
从几何压缩到KTX2纹理压缩:轻装3D的Three.js场景优化进阶
3d模型轻量化·three.js·3d模型·ktx2·轻装3d·纹理压缩
一根数据线6 天前
一键解决ThreeJS3D场景卡顿问题!轻装3D的几何体实例化与合并
3d模型轻量化·three.js·3d模型·轻装3d·实例化渲染·几何体合并
一根数据线7 天前
ThreeJS模型加载卡顿怎么办,用轻装3D来做模型压缩和LOD分级
3d模型轻量化·three.js·lod·3d模型优化·draco压缩·轻装3d
来自上海的这位朋友7 天前
用 Three.js 做一个 Web 3D 非对称追猎 Demo:从场景、角色到手感调试
后端·游戏开发·three.js
来自上海的这位朋友7 天前
Spring Boot + MySQL 搭一个多人游戏后端:登录、房间、匹配、对局和成长系统
前端·后端·three.js
郝学胜-神的一滴7 天前
中级OpenGL教程 007:解决背面光照异常高光问题
c++·unity·游戏引擎·three.js·opengl·unreal
贵州数擎科技有限公司8 天前
Three.js 的较小瀑布实现
webgl·three.js
李伟_Li慢慢10 天前
实时动画缓冲
前端·机器人·three.js