ShadowEditor/web/src/event/GPUPickEvent.js
2020-07-04 09:50:18 +08:00

253 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright 2017-2020 The ShadowEditor Authors. All rights reserved.
*
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*
* For more information, please visit: https://github.com/tengge1/ShadowEditor
* You can also visit: https://gitee.com/tengge1/ShadowEditor
*/
import BaseEvent from './BaseEvent';
import PickVertexShader from './shader/pick_vertex.glsl';
import PickFragmentShader from './shader/pick_fragment.glsl';
import DepthVertexShader from './shader/depth_vertex.glsl';
import DepthFragmentShader from './shader/depth_fragment.glsl';
import MeshUtils from '../utils/MeshUtils';
let maxHexColor = 1;
/**
* 使用GPU选取物体和计算鼠标世界坐标
* @author tengge / https://github.com/tengge1
*/
function GPUPickEvent() {
BaseEvent.call(this);
this.isIn = false;
this.offsetX = 0;
this.offsetY = 0;
this.waitTime = 10; // 10毫秒检测一次提升性能
this.oldTime = 0;
this.selectMode = 'whole';
this.onMouseMove = this.onMouseMove.bind(this);
this.onAfterRender = this.onAfterRender.bind(this);
this.onResize = this.onResize.bind(this);
this.onStorageChanged = this.onStorageChanged.bind(this);
}
GPUPickEvent.prototype = Object.create(BaseEvent.prototype);
GPUPickEvent.prototype.constructor = GPUPickEvent;
GPUPickEvent.prototype.start = function () {
app.on(`mousemove.${this.id}`, this.onMouseMove);
app.on(`afterRender.${this.id}`, this.onAfterRender);
app.on(`resize.${this.id}`, this.onResize);
app.on(`storageChanged.${this.id}`, this.onStorageChanged);
this.selectMode = app.storage.selectMode;
};
GPUPickEvent.prototype.stop = function () {
app.on(`mousemove.${this.id}`, null);
app.on(`afterRender.${this.id}`, null);
app.on(`resize.${this.id}`, null);
app.on(`storageChanged.${this.id}`, null);
this.selectMode = 'whole';
};
GPUPickEvent.prototype.onMouseMove = function (event) {
if (event.target !== app.editor.renderer.domElement) { // 鼠标不在画布上
this.isIn = false;
app.call(`gpuPick`, this, {
object: null,
point: null,
distance: 0
});
return;
}
this.isIn = true;
this.offsetX = event.offsetX;
this.offsetY = event.offsetY;
};
/**
* 由于需要较高性能,所以尽量不要拆分函数。
*/
GPUPickEvent.prototype.onAfterRender = function () {
if (!this.isIn || app.editor.gpuPickNum === 0) {
return;
}
// 间隔一段时间执行一次,提高性能
let now = new Date().getTime();
if (now - this.oldTime < this.waitTime) {
return;
}
this.oldTime = now;
let { scene, renderer } = app.editor;
const camera = app.editor.view === 'perspective' ? app.editor.camera : app.editor.orthCamera;
const { width, height } = renderer.domElement;
if (this.init === undefined) {
this.init = true;
this.depthMaterial = new THREE.ShaderMaterial({
vertexShader: DepthVertexShader,
fragmentShader: DepthFragmentShader,
uniforms: {
far: {
value: camera.far
}
}
});
this.renderTarget = new THREE.WebGLRenderTarget(width, height);
this.pixel = new Uint8Array(4);
this.nearPosition = new THREE.Vector3(); // 鼠标屏幕位置在near处的相机坐标系中的坐标
this.farPosition = new THREE.Vector3(); // 鼠标屏幕位置在far处的相机坐标系中的坐标
this.world = new THREE.Vector3(); // 通过插值计算世界坐标
this.line = new THREE.Line3(this.nearPosition, this.farPosition);
this.plane = new THREE.Plane().setFromNormalAndCoplanarPoint(new THREE.Vector3(0, 1, 0), new THREE.Vector3());
}
// 记录旧属性
const oldBackground = scene.background;
const oldOverrideMaterial = scene.overrideMaterial;
const oldRenderTarget = renderer.getRenderTarget();
// ---------------------------- 1. 使用GPU判断选中的物体 -----------------------------------
scene.background = null; // 有背景图,可能导致提取的颜色不准
scene.overrideMaterial = null;
renderer.setRenderTarget(this.renderTarget);
// 更换选取材质
scene.traverseVisible(n => {
if (!(n instanceof THREE.Mesh)) {
return;
}
n.oldMaterial = n.material;
if (n.pickMaterial) {
n.material = n.pickMaterial;
return;
}
let material = new THREE.ShaderMaterial({
vertexShader: PickVertexShader,
fragmentShader: PickFragmentShader,
uniforms: {
pickColor: {
value: new THREE.Color(maxHexColor)
}
}
});
n.pickColor = maxHexColor;
maxHexColor++;
n.material = n.pickMaterial = material;
});
// 绘制并读取像素
renderer.clear(); // 一定要清缓冲区renderer没开启自动清空缓冲区
renderer.render(scene, camera);
renderer.readRenderTargetPixels(this.renderTarget, this.offsetX, height - this.offsetY, 1, 1, this.pixel);
// 还原原来材质,并获取选中物体
const currentColor = this.pixel[0] * 0xffff + this.pixel[1] * 0xff + this.pixel[2];
let selected = null;
scene.traverseVisible(n => {
if (!(n instanceof THREE.Mesh)) {
return;
}
if (n.pickMaterial && n.pickColor === currentColor) {
selected = n;
}
if (n.oldMaterial) {
n.material = n.oldMaterial;
delete n.oldMaterial;
}
});
// ------------------------- 2. 使用GPU反算世界坐标 ----------------------------------
scene.overrideMaterial = this.depthMaterial; // 注意this.material为undifined写在这也不会报错不要写错了。
renderer.clear();
renderer.render(scene, camera);
renderer.readRenderTargetPixels(this.renderTarget, this.offsetX, height - this.offsetY, 1, 1, this.pixel);
let cameraDepth = 0;
const deviceX = this.offsetX / width * 2 - 1;
const deviceY = - this.offsetY / height * 2 + 1;
// TODO: nearPosition和farPosition命名反了
this.nearPosition.set(deviceX, deviceY, 1); // 屏幕坐标系:(0, 0, 1)
this.nearPosition.applyMatrix4(camera.projectionMatrixInverse); // 相机坐标系:(0, 0, -far)
this.farPosition.set(deviceX, deviceY, -1); // 屏幕坐标系:(0, 0, -1)
this.farPosition.applyMatrix4(camera.projectionMatrixInverse); // 相机坐标系:(0, 0, -near)
if (this.pixel[2] !== 0 || this.pixel[1] !== 0 || this.pixel[0] !== 0) { // 鼠标位置存在物体
let hex = (this.pixel[0] * 65535 + this.pixel[1] * 255 + this.pixel[2]) / 0xffffff;
if (this.pixel[3] === 0) {
hex = -hex;
}
cameraDepth = -hex * camera.far; // 相机坐标系中鼠标所在点的深度(注意:相机坐标系中的深度值为负值)
const t = (cameraDepth - this.nearPosition.z) / (this.farPosition.z - this.nearPosition.z);
this.world.set(
this.nearPosition.x + (this.farPosition.x - this.nearPosition.x) * t,
this.nearPosition.y + (this.farPosition.y - this.nearPosition.y) * t,
cameraDepth
);
this.world.applyMatrix4(camera.matrixWorld);
} else { // 鼠标位置不存在物体则与y=0的平面的交点
this.nearPosition.applyMatrix4(camera.matrixWorld); // 世界坐标系近点
this.farPosition.applyMatrix4(camera.matrixWorld); // 世界坐标系远点
this.line.set(this.nearPosition, this.farPosition);
this.plane.intersectLine(this.line, this.world);
}
// 还原原来的属性
scene.background = oldBackground;
scene.overrideMaterial = oldOverrideMaterial;
renderer.setRenderTarget(oldRenderTarget);
// ------------------------------- 3. 输出碰撞结果 --------------------------------------------
if (selected && this.selectMode === 'whole') { // 选择整体
selected = MeshUtils.partToMesh(selected);
}
app.call(`gpuPick`, this, {
object: selected, // 碰撞到的物体没碰到为null
point: this.world, // 碰撞点坐标没碰到物体与y=0平面碰撞
distance: cameraDepth // 相机到碰撞点距离
});
};
GPUPickEvent.prototype.onResize = function () {
if (!this.renderTarget) {
return;
}
const { width, height } = app.editor.renderer.domElement;
this.renderTarget.setSize(width, height);
};
GPUPickEvent.prototype.onStorageChanged = function (name, value) {
if (name === 'selectMode') {
this.selectMode = value;
}
};
export default GPUPickEvent;