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>
相关推荐
牧艺8 小时前
用 Next.js + React Three Fiber 打造 3D 快递仓储可视化
前端·three.js
Yuhua_Cesium_Threejs2 天前
《在 Cesium 中用 Three.js 实现气象雷达三维体渲染——从原理到性能优化》
three.js
牧艺2 天前
用 Three.js 实现一个浏览器端 3D 看车的项目
前端·three.js
凌涘7 天前
从零掌握 CSS 3D:用几行代码让网页"立"起来
three.js
柳杉8 天前
我用Threejs 搓了一个 3D 中国地图设计器,开箱即用
前端·three.js·数据可视化
郝学胜-神的一滴17 天前
[简化版 GAMES 101] 计算机图形学 12:可见性与 Z‑Buffer 深度缓存
unity·godot·图形渲染·three.js·opengl·unreal
VcB之殇18 天前
[Three.js] 实现两个3D模型之间的粒子化切换
前端·javascript·three.js
郝学胜-神的一滴20 天前
中级OpenGL教程 008:精准控制高光光斑大小与强度
c++·unity·godot·three.js·图形学·opengl·unreal
xier12345623 天前
three-instance-batch 开发笔记
javascript·three.js
一根数据线1 个月前
从几何压缩到KTX2纹理压缩:轻装3D的Three.js场景优化进阶
3d模型轻量化·three.js·3d模型·ktx2·轻装3d·纹理压缩