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 天前
使用@react-three/fiber,@mkkellogg/gaussian-splats-3d加载.splat,.ply,.ksplat文件
前端·react.js·three.js
格瑞@_@8 天前
11.Three.js使用indexeddb前端缓存模型优化前端加载效率
前端·javascript·缓存·three.js·indexeddb缓存
谢小飞8 天前
我做了三把椅子原来纹理这样加载切换
前端·three.js
小白菜学前端9 天前
ThreeJS创建一个3D物体的基本流程
3d·three.js
茶老翁10 天前
1-初识Three.js
前端·three.js
莫石11 天前
搓绳子(直)
前端·数学·three.js
小白菜学前端12 天前
3d 添加辅助坐标器和轨道控制器
3d·three.js
孙_华鹏14 天前
threejs——实战中材质的应用
前端·three.js·数据可视化
天涯学馆17 天前
Three.js灯光阴影与动画交互
前端·unity3d·three.js
格瑞@_@21 天前
6.Three.js贴图与uv映射(uv坐标)理解和实践
javascript·three.js·贴图·uv