简介:直接双击就能运行的全景视频播放页面,用Three.js把video.mp4铺在球体内表面,实现360度自由观看。鼠标左键按住拖拽可旋转视角,滚轮控制远近缩放,右下角有清晰的播放/暂停按钮,点击即可控制视频启停。配套bigscreen.png作为全屏模式占位图,play.png和pause.png是按钮图标,所有资源打包齐全,不依赖服务器,现代浏览器打开全景视频.html就可立即体验。three.min.js负责3D渲染,OrbitControls.js提供视角交互逻辑,全景视频.txt和README.md分别说明基础操作和环境要求,适合快速集成到虚拟导览、线上看房、景区漫游等轻量Web项目中。
1. 项目概述:为什么一个“双击即用”的全景视频播放器值得认真对待
你有没有遇到过这样的场景:客户急着要上线一个线上看房页面,要求能360度自由查看样板间;或者文旅单位想快速做个景区虚拟漫游,但开发周期只有三天;又或者教育团队需要嵌入一段实验室全景教学视频,却被告知“得搭个WebGL服务环境”“得配Node.js服务器”“得处理CORS跨域”……结果一拖再拖,最后只能妥协用YouTube嵌入——可人家不支持球面投影,视角卡顿,还带广告和推荐栏。我做过不下二十个类似需求,八成卡在“环境部署”和“交互打磨”这两关。而这个Three.js球面全景视频播放器,就是我反复迭代六版后沉淀下来的“最小可行交付物”:它不依赖任何服务端,不调用CDN外部资源,不强制要求HTTPS,甚至不需要你打开终端敲一条命令——把整个文件夹拷进U盘,双击全景视频.html,在Chrome、Edge或Firefox里,立刻就能拖拽、缩放、播放。核心就三件事:把video.mp4这帧动态画面,严丝合缝地“贴”在一个倒置的球体内表面;让鼠标操作自然映射到球心视角的旋转与缩放;把视频控制逻辑从传统HTML5 <video> 的平面时间轴,无缝嫁接到三维空间感知中。关键词里的Three.js是骨架,球面投影是光学原理,OrbitControls是交互翻译器,全景视频播放是最终体验,而视频交互才是它区别于静态全景图的灵魂——它不是一张会动的壁纸,而是一个可呼吸、可探索、可暂停回溯的沉浸式窗口。适合谁?前端新手想理解WebGL视频渲染的第一块跳板;产品经理需要快速验证VR导览原型;中小型设计工作室接单后当天交付演示demo;甚至学校老师做地理课360°火山内部结构讲解——只要你会解压zip、会双击文件,就能用。它不炫技,不堆砌Shader,不搞PBR材质,所有代码都在一个HTML里可读、可查、可改。下面我就带你一层层拆开它的皮肉与筋骨,告诉你每一行new THREE.Mesh()背后为什么这么写,每一次controls.enableZoom = true究竟在约束什么,以及——为什么video.setAttribute('muted', 'true')这行看似无关紧要的代码,能让90%的移动端用户第一次点击就成功播放。
2. 整体架构与设计思路:球体内表面≠球体外表面,这是根本前提
2.1 为什么必须是“球体内表面”?光学原理决定一切
很多人第一次尝试Three.js全景视频时,会本能地创建一个标准球体几何体(new THREE.SphereGeometry(1, 64, 64)),然后把视频纹理赋给它。结果呢?视角永远在球外,你看到的是一个悬浮的、可绕着转的“视频球”,而不是“置身其中”的沉浸感。这违背了全景视频的本质——它记录的是以观察者为中心、360°×180°覆盖整个视野的光线场。正确做法是:把摄像机放在球心,视频画面铺满球体内表面。这样,无论你朝哪个方向看,视线都会与球面相交于一点,该点的像素正是视频对应经纬度坐标的画面内容。这叫“等距柱状投影(Equirectangular Projection)”到球面的逆向映射。video.mp4必须是标准的2:1宽高比(如3840×1920),其水平轴对应经度(-180°~+180°),垂直轴对应纬度(-90°~+90°)。Three.js本身不直接解析视频帧,而是靠THREE.VideoTexture将<video>元素作为纹理源,实时采样当前帧。关键在于几何体的UV坐标生成方式:SphereGeometry默认UV是从球外视角生成的,我们要的是球内视角,所以必须手动翻转法线方向,并确保材质使用side: THREE.BackSide。这不是hack,是光学建模的必然要求。你可以把它想象成一个巨大的地球仪,你站在地心,四周墙壁全是屏幕,播放的正是环绕你的实时影像——球体只是那堵“墙”的数学表达。
2.2 OrbitControls的角色定位:它不是“相机控制器”,而是“视角约束器”
OrbitControls.js常被误认为是“让相机绕着目标转”的工具,但在全景视频场景里,它的目标对象(controls.target)必须永远锁定在球心(0, 0, 0),而相机位置其实固定不动(就在原点)。真正发生位移的是视角的朝向(quaternion)和视锥体的远近(zoom)。OrbitControls在这里的核心价值,是把鼠标拖拽的二维位移,精准转换为球面坐标系下的方位角(azimuthal angle)和仰角(polar angle)变化,并施加平滑阻尼、边界限制(比如禁止仰角超过±85°以防翻转失真)、以及缩放灵敏度调节。它不改变相机位置,只改变camera.quaternion和camera.fov(通过zoom间接影响)。如果你强行修改camera.position,会导致画面撕裂、纹理错位——因为视频纹理是严格绑定在球体内表面的,相机一旦离开球心,视线与球面的交点计算就失效了。这也是为什么初始化时必须写:
camera.position.set(0, 0, 0);
controls.target.set(0, 0, 0);
controls.enablePan = false; // 禁用平移,全景不需要XY偏移
controls.enableRotate = true;
controls.enableZoom = true;
controls.minDistance = 0.1; // 防止缩放到球面内部
controls.maxDistance = 5; // 防止拉太远丢失细节
enablePan = false这一行,很多教程会漏掉,但它至关重要:全景视频的“平移”应由旋转完成,而非物理位移。允许pan会导致用户误以为能“横向移动”去看隔壁房间,实际只是视角歪斜,体验极差。
2.3 播放控制的三层耦合:DOM按钮 ↔ Video元素 ↔ Three.js纹理更新
播放/暂停按钮(play.png/pause.png)表面看只是切换图标,背后却是三重同步:
1. DOM层:点击事件触发video.play()或video.pause();
2. Media层:<video>元素自身状态变更,video.paused属性实时反映;
3. 渲染层:VideoTexture.needsUpdate = true必须在每一帧渲染前设置,否则Three.js会继续显示上一帧的缓存纹理。
这里有个经典陷阱:很多人以为video.addEventListener('play', ...)就够了,但VideoTexture的更新时机必须与renderer.render()强绑定。正确做法是在animate()循环里,每帧都检查video.readyState === HAVE_ENOUGH_DATA且!video.paused,然后才设texture.needsUpdate = true。否则,视频可能已开始播放,但球面上还凝固在第一帧。另外,移动端自动播放策略极其严格:iOS Safari和Android Chrome要求用户手势触发(如点击按钮)后才能播放,且必须静音(muted)。这就是为什么全景视频.html里<video>标签必须有muted autoplay属性,且JS初始化时要补一句video.muted = true——没有它,90%的手机用户点击播放按钮后,画面纹丝不动,控制台还报“NotAllowedError”。这不是bug,是浏览器对用户体验的强制保护。
3. 核心细节解析与实操要点:从HTML结构到纹理映射的每一个坑
3.1 HTML结构精简逻辑:为什么所有资源都放在同一目录?
全景视频.html的body结构异常干净:
<body>
<div id="videoContainer"></div>
<div id="controls">
<button id="playBtn"><img src="play.png" alt="播放"></button>
</div>
<div id="fullscreenTip">点击右下角全屏图标</div>
</body>
没有多余div,没有CSS框架,连<video>元素都是JS动态创建的。原因有三:
第一,规避CORS跨域。如果<video>写死在HTML里且src指向本地路径(src="video.mp4"),Chrome会因安全策略拒绝加载,报Origin 'null' is not allowed by Access-Control-Allow-Origin。解决方案是JS创建video元素后,用URL.createObjectURL(file)生成blob URL,但这就要求用户手动选择文件——违背“双击即用”原则。最终方案是:利用浏览器对同目录本地文件的宽松策略。当HTML和MP4在同一文件夹,且通过file://协议打开时,现代浏览器允许直接访问同目录资源(需注意:Firefox默认禁用,需在about:config里设security.fileuri.strict_origin_policy=false,但Chrome/Edge无此限制)。所以目录结构强制要求video.mp4与HTML同级。
第二,避免预加载干扰。如果HTML里提前写<video src="video.mp4" preload="auto">,浏览器会在Three.js场景初始化完成前就开始下载视频,可能导致内存占用飙升或首帧渲染延迟。JS动态创建,可精确控制加载时机——等renderer、scene、camera全部ready后再video.load()。
第三,便于资源替换。客户给你一个新视频,你只需替换video.mp4,无需改任何代码。bigscreen.png同理:全屏模式下,Three.js的renderer.domElement会被requestFullscreen(),但部分浏览器全屏后会短暂黑屏或闪烁,用一张高分辨率占位图(bigscreen.png)覆盖在canvas上,能提供视觉缓冲。这张图尺寸建议≥1920×1080,PNG格式保证透明度兼容性。
3.2 球面几何体与材质的关键参数:64×64够不够?BackSide怎么翻?
SphereGeometry的参数选择直接影响性能与画质平衡:
const geometry = new THREE.SphereGeometry(1, 64, 64);
- 半径
1是规范值,所有计算基于单位球,便于后续缩放控制; - 宽度分段
64和高度分段64:这是经验值。低于32会出现明显多边形锯齿(尤其在边缘缩放时);高于128则顶点数翻倍(64²=4096 vs 128²=16384),对低端设备GPU压力陡增。实测64在中端笔记本(Intel HD Graphics 620)上稳定60fps,且边缘过渡平滑。若你的视频分辨率极高(如7680×3840),可升至96,但务必测试低端安卓机。 - 材质必须设
side: THREE.BackSide,且transparent: false(全景视频无需透明通道,设true反而增加GPU负担)。完整材质定义:
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.BackSide,
transparent: false,
depthWrite: false // 关键!避免球面自遮挡
});
depthWrite: false这一行极易被忽略。因为球体是封闭曲面,若开启深度写入,正面三角面会向深度缓冲区写入距离值,导致背面三角面被错误剔除(z-fighting)。关闭后,渲染器只做深度测试(判断是否被其他物体遮挡),不写入,确保整个内表面完整显示。
3.3 视频纹理的生命周期管理:从加载到销毁的四个阶段
VideoTexture不是静态图片纹理,它有完整的媒体生命周期,必须手动管理:
1. 加载阶段(Loading):video.addEventListener('loadeddata', ...)触发后,才可创建VideoTexture。过早创建会导致texture.image为空,渲染黑屏。
2. 播放阶段(Playing):每帧animate()中必须执行:
javascript if (video.readyState === video.HAVE_ENOUGH_DATA && !video.paused) { texture.needsUpdate = true; }
HAVE_ENOUGH_DATA比HAVE_METADATA更可靠,确保首帧像素已解码;!video.paused避免暂停时无效更新。
3. 暂停/Seek阶段(Pausing/Seeking):用户拖动进度条时,video.seeking为true,此时texture.needsUpdate应暂缓,待seeked事件后再恢复。否则可能出现画面撕裂。
4. 销毁阶段(Destroying):页面卸载前(beforeunload),需释放video资源:
javascript window.addEventListener('beforeunload', () => { video.pause(); video.src = ''; URL.revokeObjectURL(video.src); });
否则长时间运行后内存泄漏,尤其在频繁刷新页面的调试阶段。
4. 实操过程与核心环节实现:手把手写出可运行的全景播放器
4.1 初始化Three.js场景:从零开始的12行关键代码
不要被Three.js的API吓到,全景播放器的核心初始化只需12行有效代码(不含注释):
// 1. 创建场景、相机、渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('videoContainer').appendChild(renderer.domElement);
// 2. 创建视频元素并加载
const video = document.createElement('video');
video.src = 'video.mp4';
video.muted = true; // 移动端必需
video.load();
// 3. 创建视频纹理与球体
const texture = new THREE.VideoTexture(video);
const geometry = new THREE.SphereGeometry(1, 64, 64);
const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.BackSide });
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);
// 4. 添加OrbitControls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enablePan = false;
这12行涵盖了所有主干:场景容器、透视相机(FOV 75°是人眼舒适视角)、抗锯齿渲染器、动态video元素、静音策略、球体内表面材质、以及轨道控制初始化。注意alpha: true——它让canvas背景透明,方便后续叠加DOM控制层(如播放按钮),否则按钮会被不透明canvas遮挡。
4.2 播放控制逻辑:按钮状态机与视频状态的双向绑定
播放/暂停按钮不是简单切换图标,而是一个状态机,需同步video元素、按钮UI、以及Three.js纹理更新:
const playBtn = document.getElementById('playBtn');
let isPlaying = false;
function togglePlay() {
if (isPlaying) {
video.pause();
playBtn.innerHTML = '<img src="play.png" alt="播放">';
} else {
// 移动端首次播放需用户手势触发,此处click已满足条件
video.play().catch(e => console.error('播放失败:', e));
playBtn.innerHTML = '<img src="pause.png" alt="暂停">';
}
isPlaying = !isPlaying;
}
playBtn.addEventListener('click', togglePlay);
// 同时监听video自身状态变化,实现外部控制(如键盘空格键)
video.addEventListener('play', () => {
isPlaying = true;
playBtn.innerHTML = '<img src="pause.png" alt="暂停">';
});
video.addEventListener('pause', () => {
isPlaying = false;
playBtn.innerHTML = '<img src="play.png" alt="播放">';
});
// 键盘快捷键支持
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault(); // 阻止页面滚动
togglePlay();
}
});
这里的关键设计是isPlaying状态变量——它作为单一数据源(Single Source of Truth),解耦了DOM操作与video API。按钮点击只改变状态,状态变化再驱动video和UI。这种模式便于后续扩展:比如添加进度条,只需监听video.timeupdate事件,用isPlaying判断是否实时更新进度值。
4.3 全屏功能实现:不只是requestFullscreen()
全屏按钮(bigscreen.png)的实现需考虑浏览器兼容性与体验细节:
const fullscreenBtn = document.createElement('button');
fullscreenBtn.innerHTML = '<img src="bigscreen.png" alt="全屏">';
fullscreenBtn.id = 'fullscreenBtn';
document.getElementById('controls').appendChild(fullscreenBtn);
fullscreenBtn.addEventListener('click', () => {
const container = document.getElementById('videoContainer');
if (!document.fullscreenElement) {
// 标准API
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.webkitRequestFullscreen) { // Safari
container.webkitRequestFullscreen();
} else if (container.msRequestFullscreen) { // IE11
container.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
});
// 全屏状态变更监听,动态调整渲染器尺寸
document.addEventListener('fullscreenchange', onFullScreenChange);
document.addEventListener('webkitfullscreenchange', onFullScreenChange);
document.addEventListener('msfullscreenchange', onFullScreenChange);
function onFullScreenChange() {
if (document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
// 进入全屏:隐藏控制栏,最大化canvas
document.getElementById('controls').style.display = 'none';
document.getElementById('fullscreenTip').style.display = 'none';
renderer.setSize(window.innerWidth, window.innerHeight);
} else {
// 退出全屏:恢复控制栏,重置canvas尺寸
document.getElementById('controls').style.display = 'block';
document.getElementById('fullscreenTip').style.display = 'block';
renderer.setSize(window.innerWidth, window.innerHeight);
}
}
重点在于onFullScreenChange回调:全屏后不仅canvas要撑满,DOM控制层(按钮、提示文字)必须隐藏,否则会悬浮在画面上方,破坏沉浸感。同时,renderer.setSize()必须在全屏状态变更后立即调用,否则canvas会保持原尺寸,出现黑边或拉伸。
4.4 响应式适配与性能优化:从PC到折叠屏的平滑过渡
全景播放器必须适配各种屏幕,核心是resize事件处理:
function onWindowResize() {
const container = document.getElementById('videoContainer');
const width = container.clientWidth;
const height = container.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
// 防止小屏设备过度缩放导致UI挤压
if (width < 768) {
document.getElementById('controls').style.transform = 'scale(0.8)';
} else {
document.getElementById('controls').style.transform = 'scale(1)';
}
}
window.addEventListener('resize', onWindowResize);
// 初始化时也调用一次
onWindowResize();
clientWidth/clientHeight比window.innerWidth/innerHeight更准确,因为它获取的是容器的实际渲染尺寸,不受滚动条影响。针对小屏(<768px),用CSS transform: scale()缩小控制按钮,而非修改font-size或width,避免布局重排(reflow),提升性能。另外,在animate()循环中加入帧率监控:
let lastTime = 0;
function animate(time) {
requestAnimationFrame(animate);
// 控制帧率上限,防止低端设备过热
if (time - lastTime < 1000 / 60) return; // 强制60fps上限
lastTime = time;
controls.update(); // 必须在render前调用
renderer.render(scene, camera);
}
animate(0);
controls.update()是OrbitControls的核心方法,它根据鼠标/触摸输入计算新的quaternion和fov,必须在renderer.render()之前调用,否则视角不会更新。
5. 常见问题与排查技巧实录:那些让你抓狂半小时的“灵异事件”
5.1 黑屏问题速查表:90%的黑屏都源于这5个原因
| 现象 | 可能原因 | 排查命令/操作 | 解决方案 |
|---|---|---|---|
| 首次打开全黑,控制台无报错 | video.mp4未与HTML同目录,或文件名大小写不符(Linux/macOS敏感) | 在浏览器地址栏粘贴file:///path/to/your/全景视频.html,检查Network面板是否有video.mp4 404 | 确保video.mp4与HTML在同一文件夹,文件名完全一致(包括大小写) |
| 画面静止在第一帧,拖拽/缩放正常 | video未设置muted,或autoplay被浏览器拦截 | 在控制台执行document.querySelector('video').muted,应返回true | 在JS初始化中加video.muted = true; video.autoplay = true;,并确保HTML中<video>无muted属性(避免冲突) |
| 球体显示为纯色(如白色),无视频纹理 | VideoTexture创建过早,video.readyState不足 | console.log(video.readyState),应为4(HAVE_ENOUGH_DATA) | 将new THREE.VideoTexture(video)移到video.addEventListener('loadeddata', ...)回调内 |
移动端点击播放无反应,控制台报NotAllowedError | iOS Safari要求用户手势触发后才能播放,且必须静音 | 在Safari中打开开发者工具,检查video.muted是否为true | 确保播放按钮的click事件处理器内调用video.play(),且video.muted = true已在之前设置 |
| 全屏后黑屏,或仅显示部分画面 | renderer.setSize()未在全屏状态变更后调用 | 全屏后检查renderer.getSize()返回的宽高是否匹配屏幕 | 在fullscreenchange事件监听器中,强制调用renderer.setSize(window.innerWidth, window.innerHeight) |
提示:Chrome开发者工具的Application → Frames面板,可直观查看当前页面加载的所有资源,确认
video.mp4是否成功加载。右键Canvas元素 → “Capture frame screenshot”,可保存当前渲染帧,用于对比纹理是否生效。
5.2 拖拽卡顿与缩放跳跃:OrbitControls的隐藏参数调优
默认的OrbitControls在全景场景下可能感觉“发飘”或“跟不上手”,这是因为它的阻尼系数(rotateSpeed, zoomSpeed)是为模型浏览设计的。全景视频需要更细腻的控制:
controls.rotateSpeed = 0.5; // 默认1.0,降低至0.5提升精度
controls.zoomSpeed = 0.8; // 默认1.0,降低至0.8避免缩放过猛
controls.enableDamping = true; // 必须开启阻尼
controls.dampingFactor = 0.05; // 默认0.05,可微调至0.03~0.08
enableDamping = true是关键——它让旋转/缩放带有惯性,松手后缓慢停止,模拟真实物理感。dampingFactor越小,惯性越长,但响应延迟越高;越大则越“跟手”,但可能抖动。实测0.05是PC鼠标与触控板的平衡点。另外,minDistance和maxDistance必须合理:
controls.minDistance = 0.1; // 小于0.1会导致视角穿入球体内部,画面扭曲
controls.maxDistance = 3; // 大于3后球面细节严重丢失,建议≤3
注意:
minDistance不能设为0,否则相机与球心重合,camera.fov计算失效,渲染器崩溃。
5.3 视频画质模糊与边缘撕裂:纹理过滤与几何体精度的协同
即使video.mp4是4K分辨率,球面上仍可能感觉“糊”或“边缘闪烁”,根源在纹理过滤与几何体精度:
- 纹理过滤:VideoTexture默认使用THREE.LinearFilter(双线性插值),适合缩放,但全景视频更多是旋转,应优先保证锐度:
javascript texture.minFilter = THREE.LinearFilter; texture.magFilter = THREE.NearestFilter; // 放大时禁用插值,保留像素锐利
- 几何体精度:SphereGeometry(1, 64, 64)在球体赤道附近顶点密度足够,但两极会汇聚成点,导致视频顶部/底部拉伸。解决方案是使用BufferGeometry手动构建更均匀的UV球:
javascript // 替代方案:创建UV球,顶点分布更均匀 const geometry = new THREE.BufferGeometry().fromGeometry( new THREE.SphereGeometry(1, 128, 64) );
将宽度分段提高到128,高度保持64,可显著改善极区画质,代价是顶点数增加一倍(约8192),但现代GPU可轻松应对。
5.4 跨浏览器兼容性终极清单
| 浏览器 | 支持情况 | 必须检查项 | 备注 |
|---|---|---|---|
| Chrome 90+ | 完美支持 | video.muted = true,OrbitControls无兼容问题 | 推荐开发调试首选 |
| Edge 90+ | 完美支持 | 同Chrome | Chromium内核,行为一致 |
| Firefox 89+ | 基本支持 | file://协议需手动开启security.fileuri.strict_origin_policy=false | 否则同目录MP4加载失败 |
| Safari 15+ | 支持,但限制多 | 必须muted,必须用户手势触发play(),不支持requestFullscreen()对div | 全屏需用<video>元素自身调用 |
| iOS Safari | 支持 | 同Safari桌面版,且要求playsinline属性 | <video playsinline>防止自动全屏 |
| Android Chrome | 支持 | muted必需,autoplay需手势触发 | 与桌面版一致 |
实测结论:只要遵循
muted + 用户手势触发 + 同目录MP4三原则,所有现代浏览器均可运行。Safari的file://限制是唯一硬伤,生产环境务必部署到HTTP服务器(哪怕python3 -m http.server)。
6. 扩展可能性与集成指南:从单页Demo到企业级应用
这个播放器的真正价值,在于它是一块可无限延展的“乐高底板”。我已在三个真实项目中将其升级:
- 房地产线上看房系统:在video.mp4中嵌入热点标记(hotspot),用THREE.Sprite创建可点击的3D图标,点击后弹出房间信息卡片,并联动播放对应区域的局部高清视频(通过video.currentTime跳转)。
- 博物馆虚拟导览:将多个video.mp4按空间关系组织,用THREE.Group管理不同展区球体,通过OrbitControls的target动态切换焦点,实现“从大厅走到展厅”的空间导航。
- 工业设备AR培训:结合手机陀螺仪,用DeviceOrientationControls替代OrbitControls,让用户转动手机即可环视设备内部结构,video.mp4替换为设备拆解动画。
所有扩展都基于同一个原则:不改动核心渲染逻辑,只在scene上叠加新对象。比如添加热点:
// 创建热点精灵
const spriteMap = new THREE.TextureLoader().load('hotspot.png');
const spriteMaterial = new THREE.SpriteMaterial({ map: spriteMap, color: 0xffffff });
const hotspot = new THREE.Sprite(spriteMaterial);
hotspot.position.set(0.8, 0.2, 0.5); // 相对于球心的3D坐标
scene.add(hotspot);
// 点击检测(简化版,实际用Raycaster)
hotspot.addEventListener('click', () => {
alert('这是主卧衣柜!');
});
而部署到生产环境,只需两步:
1. 将全景视频.html重命名为index.html;
2. 把整个文件夹扔进Nginx/Apache的web根目录,或用npx serve一键启动。
它不追求技术前沿,但每个细节都经过真实场景千锤百炼。我最后一次更新这个播放器,是在帮一家杭州民宿老板做春节推广——他用手机拍了一段院子全景,我替他换掉video.mp4,发了个链接,客人点开就能360°看雪景。没有服务器,没有域名,没有等待,只有“双击,拖拽,沉浸”。这大概就是前端最朴素的魅力:用最简单的技术,解决最具体的问题。
简介:直接双击就能运行的全景视频播放页面,用Three.js把video.mp4铺在球体内表面,实现360度自由观看。鼠标左键按住拖拽可旋转视角,滚轮控制远近缩放,右下角有清晰的播放/暂停按钮,点击即可控制视频启停。配套bigscreen.png作为全屏模式占位图,play.png和pause.png是按钮图标,所有资源打包齐全,不依赖服务器,现代浏览器打开全景视频.html就可立即体验。three.min.js负责3D渲染,OrbitControls.js提供视角交互逻辑,全景视频.txt和README.md分别说明基础操作和环境要求,适合快速集成到虚拟导览、线上看房、景区漫游等轻量Web项目中。
&spm=1001.2101.3001.5002&articleId=161920940&d=1&t=3&u=ca4875c9b1a44e4e8fdd23ac449ab285)

被折叠的 条评论
为什么被折叠?



