Init
This commit is contained in:
11
src/App.vue
Normal file
11
src/App.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import MaterialView from './components/MaterialView.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MaterialView></MaterialView>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
251
src/common/CameraControls.ts
Normal file
251
src/common/CameraControls.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import { Vector3 } from 'three';
|
||||
import { Viewer } from './Viewer';
|
||||
import { KeyBoard, MouseKey } from './KeyEnum';
|
||||
|
||||
|
||||
//相机控制状态
|
||||
export enum CameraControlState
|
||||
{
|
||||
Null = 0, Pan = 1, Rotate = 2, Scale = 3
|
||||
}
|
||||
|
||||
export class CameraControls
|
||||
{
|
||||
m_TouthTypeList = [CameraControlState.Rotate, CameraControlState.Scale, CameraControlState.Pan];
|
||||
m_domElement: HTMLElement;//HTMLDocument
|
||||
//起始点击
|
||||
m_StartClickPoint: THREE.Vector3 = new THREE.Vector3();
|
||||
m_EndClickPoint: THREE.Vector3 = new THREE.Vector3();
|
||||
m_DollyStart: THREE.Vector2 = new THREE.Vector2();
|
||||
m_DollyEnd: THREE.Vector2 = new THREE.Vector2();
|
||||
|
||||
m_KeyDown = new Map<KeyBoard, boolean>();
|
||||
m_MouseDown = new Map<MouseKey, boolean>();
|
||||
|
||||
//状态
|
||||
m_State: CameraControlState = CameraControlState.Null;
|
||||
m_Viewer: Viewer;
|
||||
//左键使用旋转
|
||||
m_LeftUseRotate: boolean = true;
|
||||
|
||||
constructor(viewer: Viewer)
|
||||
{
|
||||
this.m_Viewer = viewer;
|
||||
this.m_domElement = viewer.Renderer.domElement.parentElement;
|
||||
this.RegisterEvent();
|
||||
}
|
||||
|
||||
RegisterEvent()
|
||||
{
|
||||
if (this.m_domElement)
|
||||
{
|
||||
this.m_domElement.addEventListener("mousedown", this.onMouseDown, false)
|
||||
this.m_domElement.addEventListener("mousemove", this.onMouseMove, false)
|
||||
this.m_domElement.addEventListener("mouseup", this.onMouseUp, false)
|
||||
window.addEventListener("keydown", this.onKeyDown, false);
|
||||
window.addEventListener("keyup", this.onKeyUp, false);
|
||||
this.m_domElement.addEventListener('wheel', this.onMouseWheel, false);
|
||||
|
||||
this.m_domElement.addEventListener('touchstart', this.onTouchStart, false);
|
||||
this.m_domElement.addEventListener('touchend', this.onTouchEnd, false);
|
||||
this.m_domElement.addEventListener('touchmove', this.onTouchMove, false);
|
||||
|
||||
window.addEventListener("blur", this.onBlur, false);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 窗体失去焦点时.
|
||||
* @memberof CameraControls
|
||||
*/
|
||||
onBlur = () =>
|
||||
{
|
||||
this.m_KeyDown.clear();
|
||||
this.m_MouseDown.clear();
|
||||
}
|
||||
|
||||
//触屏开始事件
|
||||
onTouchStart = (event: TouchEvent) =>
|
||||
{
|
||||
this.m_Viewer.UpdateLockTarget();
|
||||
this.m_StartClickPoint.set(event.touches[0].pageX, event.touches[0].pageY, 0);
|
||||
if (event.touches.length < 4)
|
||||
{
|
||||
if (event.touches.length == 2)
|
||||
{
|
||||
var dx = event.touches[0].pageX - event.touches[1].pageX;
|
||||
var dy = event.touches[0].pageY - event.touches[1].pageY;
|
||||
var distance = Math.sqrt(dx * dx + dy * dy);
|
||||
this.m_DollyStart.set(0, distance);
|
||||
}
|
||||
this.m_State = this.m_TouthTypeList[event.touches.length - 1];
|
||||
}
|
||||
}
|
||||
onTouchEnd = (event: TouchEvent) =>
|
||||
{
|
||||
this.m_State = CameraControlState.Null;
|
||||
}
|
||||
onTouchMove = (event: TouchEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.m_EndClickPoint.set(event.touches[0].pageX, event.touches[0].pageY, 0);
|
||||
|
||||
let vec = this.m_EndClickPoint.clone().sub(this.m_StartClickPoint);
|
||||
switch (this.m_State)
|
||||
{
|
||||
case CameraControlState.Pan:
|
||||
{
|
||||
this.m_Viewer.Pan(vec);
|
||||
break;
|
||||
}
|
||||
case CameraControlState.Scale:
|
||||
{
|
||||
var dx = event.touches[0].pageX - event.touches[1].pageX;
|
||||
var dy = event.touches[0].pageY - event.touches[1].pageY;
|
||||
|
||||
var distance = Math.sqrt(dx * dx + dy * dy);
|
||||
this.m_DollyEnd.set(0, distance);
|
||||
if (distance > this.m_DollyStart.y)
|
||||
{
|
||||
this.m_Viewer.Zoom(0.95);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.m_Viewer.Zoom(1.05)
|
||||
}
|
||||
this.m_DollyStart.copy(this.m_DollyEnd);
|
||||
break;
|
||||
}
|
||||
case CameraControlState.Rotate:
|
||||
{
|
||||
this.m_Viewer.Rotate(vec.multiplyScalar(2));
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.m_StartClickPoint.copy(this.m_EndClickPoint);
|
||||
this.m_Viewer.UpdateRender();
|
||||
}
|
||||
beginRotate()
|
||||
{
|
||||
this.m_State = CameraControlState.Rotate;
|
||||
this.m_Viewer.UpdateLockTarget();
|
||||
}
|
||||
|
||||
//最后一次按中键的时间
|
||||
lastMiddleClickTime = 0;
|
||||
//鼠标
|
||||
onMouseDown = (event: MouseEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
let key: MouseKey = event.button;
|
||||
this.m_MouseDown.set(key, true);
|
||||
this.m_StartClickPoint.set(event.offsetX, event.offsetY, 0);
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case MouseKey.Left:
|
||||
{
|
||||
if (this.m_LeftUseRotate)
|
||||
{
|
||||
this.beginRotate();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MouseKey.Middle:
|
||||
{
|
||||
let curTime = Date.now();
|
||||
let t = curTime - this.lastMiddleClickTime;
|
||||
this.lastMiddleClickTime = curTime;
|
||||
if (t < 350)
|
||||
{
|
||||
this.m_Viewer.ZoomAll();
|
||||
return;
|
||||
}
|
||||
if (this.m_KeyDown.get(KeyBoard.Control))
|
||||
{
|
||||
this.beginRotate();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.m_State = CameraControlState.Pan;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MouseKey.Right:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
onMouseUp = (event: MouseEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
this.m_State = CameraControlState.Null;
|
||||
this.m_MouseDown.set(event.button, false);
|
||||
}
|
||||
onMouseMove = (event: MouseEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
this.m_EndClickPoint.set(event.offsetX, event.offsetY, 0);
|
||||
let changeVec = this.m_EndClickPoint.clone().sub(this.m_StartClickPoint);
|
||||
this.m_StartClickPoint.copy(this.m_EndClickPoint);
|
||||
if (
|
||||
(this.m_LeftUseRotate ||
|
||||
(this.m_KeyDown.get(KeyBoard.Control))
|
||||
)
|
||||
&& this.m_State == CameraControlState.Rotate
|
||||
)
|
||||
{
|
||||
this.m_Viewer.Rotate(changeVec);
|
||||
}
|
||||
switch (this.m_State)
|
||||
{
|
||||
case CameraControlState.Pan:
|
||||
{
|
||||
this.m_Viewer.Pan(changeVec);
|
||||
break;
|
||||
}
|
||||
case CameraControlState.Rotate:
|
||||
{
|
||||
break;
|
||||
}
|
||||
case CameraControlState.Scale:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 鼠标滚轮事件
|
||||
*
|
||||
* @memberof CameraControls
|
||||
*/
|
||||
onMouseWheel = (event: WheelEvent) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
|
||||
let pt = new THREE.Vector3(event.offsetX, event.offsetY, 0);
|
||||
|
||||
this.m_Viewer.ScreenToWorld(pt, new Vector3().setFromMatrixColumn(this.m_Viewer.m_Camera.Camera.matrixWorld, 2));
|
||||
if (event.deltaY < 0)
|
||||
{
|
||||
this.m_Viewer.Zoom(0.6, pt);
|
||||
}
|
||||
else if (event.deltaY > 0)
|
||||
{
|
||||
this.m_Viewer.Zoom(1.4, pt);
|
||||
}
|
||||
}
|
||||
//按键
|
||||
onKeyDown = (event: KeyboardEvent) =>
|
||||
{
|
||||
this.m_KeyDown.set(event.keyCode, true);
|
||||
}
|
||||
onKeyUp = (event: KeyboardEvent) =>
|
||||
{
|
||||
this.m_KeyDown.set(event.keyCode, false);
|
||||
}
|
||||
}
|
290
src/common/GeUtils.ts
Normal file
290
src/common/GeUtils.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { Box3, Geometry, Line, Matrix4, Mesh, Object3D, Vector2, Vector3, type Vector } from 'three';
|
||||
import { Matrix2 } from './Matrix2';
|
||||
|
||||
|
||||
export const cZeroVec = new Vector3();
|
||||
export const cXAxis = new Vector3(1, 0, 0);
|
||||
export const cYAxis = new Vector3(0, 1, 0);
|
||||
export const cZAxis = new Vector3(0, 0, 1);
|
||||
|
||||
/**
|
||||
* 旋转一个点,旋转中心在原点
|
||||
*
|
||||
* @export
|
||||
* @param {Vector3} pt 点
|
||||
* @param {number} ang 角度.
|
||||
* @returns {Vector3} 返回pt不拷贝.
|
||||
*/
|
||||
export function rotatePoint(pt: Vector3, ang: number): Vector3
|
||||
{
|
||||
new Matrix2().setRotate(ang).applyVector(pt);
|
||||
return pt;
|
||||
}
|
||||
|
||||
export function equaln(v1: number, v2: number, fuzz = 1e-3)
|
||||
{
|
||||
return Math.abs(v1 - v2) < fuzz;
|
||||
}
|
||||
|
||||
export function equalv3(v1: Vector3, v2: Vector3, fuzz = 1e-8)
|
||||
{
|
||||
return equaln(v1.x, v2.x, fuzz) && equaln(v1.y, v2.y, fuzz) && equaln(v1.z, v2.z, fuzz);
|
||||
}
|
||||
|
||||
export function equal<T extends Vector>(v1: T, v2: T)
|
||||
{
|
||||
return v1.distanceToSquared(v2) < 1e-8;
|
||||
}
|
||||
|
||||
export function fixAngle(an: number, fixAngle: number, fuzz: number = 0.1)
|
||||
{
|
||||
if (an < 0)
|
||||
an += Math.PI * 2;
|
||||
an += fuzz;
|
||||
let rem = an % fixAngle;
|
||||
if (rem < fuzz * 2)
|
||||
{
|
||||
an -= rem;
|
||||
}
|
||||
else
|
||||
{
|
||||
an -= fuzz;
|
||||
}
|
||||
return an;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照极坐标的方式移动一个点
|
||||
*
|
||||
* @export
|
||||
* @template
|
||||
* @param {T} v 向量(2d,3d)
|
||||
* @param {number} an 角度
|
||||
* @param {number} dis 距离
|
||||
* @returns {T}
|
||||
*/
|
||||
export function polar<T extends Vector2 | Vector3>(v: T, an: number, dis: number): T
|
||||
{
|
||||
v.x += Math.cos(an) * dis;
|
||||
v.y += Math.sin(an) * dis;
|
||||
return v;
|
||||
}
|
||||
|
||||
export function angle(v: Vector3 | Vector2)
|
||||
{
|
||||
if (equaln(v.y, 0) && v.x > 0)
|
||||
return 0;
|
||||
let angle = Math.atan2(v.y, v.x);
|
||||
if (angle < 0) angle += Math.PI * 2;
|
||||
return angle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 求两个向量的夹角,顺时针为负,逆时针为正
|
||||
*
|
||||
* @param {Vector3} v1
|
||||
* @param {Vector3} v2
|
||||
* @param {Vector3} [ref] 参考向量,如果为世界坐标系则为0,0,1
|
||||
* @returns
|
||||
*/
|
||||
export function angleTo(v1: Vector3, v2: Vector3, ref: Vector3 = new Vector3(0, 0, 1))
|
||||
{
|
||||
if (!ref.equals(new Vector3(0, 0, 1)))
|
||||
{
|
||||
//任意轴坐标系. 使用相机的构造矩阵.
|
||||
ref.multiplyScalar(-1);
|
||||
let up = getLoocAtUpVec(ref);
|
||||
let refOcs = new Matrix4();
|
||||
refOcs.lookAt(cZeroVec, ref, up);
|
||||
let refOcsInv = new Matrix4().getInverse(refOcs);
|
||||
v1.applyMatrix4(refOcsInv);
|
||||
v2.applyMatrix4(refOcsInv);
|
||||
v1.z = 0;
|
||||
v2.z = 0;
|
||||
}
|
||||
if (v1.equals(cZeroVec) || v2.equals(cZeroVec))
|
||||
return 0;
|
||||
let cv = new Vector3().crossVectors(v1, v2).normalize();
|
||||
return cv.z === 0 ? v1.angleTo(v2) : v1.angleTo(v2) * cv.z;
|
||||
}
|
||||
|
||||
export function getLoocAtUpVec(dir: Vector3): Vector3
|
||||
{
|
||||
if (dir.equals(cZeroVec))
|
||||
{
|
||||
throw ("zero vector");
|
||||
}
|
||||
let norm = dir.clone().normalize();
|
||||
if (norm.equals(cZAxis))
|
||||
{
|
||||
return new Vector3(0, 1, 0);
|
||||
}
|
||||
else if (norm.equals(cZAxis.clone().negate()))
|
||||
{
|
||||
return new Vector3(0, -1, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
let xv: Vector3 = new Vector3();
|
||||
xv.crossVectors(cZAxis, norm);
|
||||
|
||||
let up = new Vector3();
|
||||
up.crossVectors(norm, xv);
|
||||
return up;
|
||||
}
|
||||
}
|
||||
|
||||
export function createLookAtMat4(dir: Vector3): Matrix4
|
||||
{
|
||||
let up = getLoocAtUpVec(dir);
|
||||
let mat = new Matrix4();
|
||||
mat.lookAt(cZeroVec, dir, up);
|
||||
return mat;
|
||||
}
|
||||
|
||||
export function isParallelTo(v1: Vector3, v2: Vector3)
|
||||
{
|
||||
return v1.clone().cross(v2).lengthSq() < 1e-9;
|
||||
}
|
||||
|
||||
export function ptToString(v: Vector3, fractionDigits: number = 3): string
|
||||
{
|
||||
return v.toArray().map(o =>
|
||||
{
|
||||
return o.toFixed(fractionDigits);
|
||||
}).join(",");
|
||||
}
|
||||
|
||||
export function midPoint(v1: Vector3, v2: Vector3): Vector3
|
||||
{
|
||||
return v1.clone().add(v2).multiplyScalar(0.5);
|
||||
}
|
||||
export function midPoint2(v1: Vector2, v2: Vector2): Vector2
|
||||
{
|
||||
return v1.clone().add(v2).multiplyScalar(0.5);
|
||||
}
|
||||
|
||||
export function midPtCir(v1: Vector3, v2: Vector3)
|
||||
{
|
||||
let baseline = new Vector3(1, 0, 0);
|
||||
let outLine = v2.clone().sub(v1);
|
||||
let ang = angleTo(baseline, outLine) / 2;
|
||||
let midLine = rotatePoint(outLine, -ang);
|
||||
return v1.clone().add(midLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得Three对象的包围盒.
|
||||
* @param obj
|
||||
* @param [updateMatrix] 是否应该更新对象矩阵
|
||||
* @returns box
|
||||
*/
|
||||
export function GetBox(obj: Object3D, updateMatrix?: boolean): Box3
|
||||
{
|
||||
let box = new Box3();
|
||||
if (updateMatrix) obj.updateMatrixWorld(false);
|
||||
if (!obj.visible) return box;
|
||||
|
||||
obj.traverse(o =>
|
||||
{
|
||||
//因为实体Erase时,实体仍然保存在Scene中.
|
||||
if (o.visible === false)
|
||||
return;
|
||||
|
||||
//@ts-ignore
|
||||
let geo = o.geometry as BufferGeometry;
|
||||
if (geo)
|
||||
{
|
||||
if (!geo.boundingBox)
|
||||
geo.computeBoundingBox();
|
||||
box.union(geo.boundingBox.clone().applyMatrix4(o.matrixWorld));
|
||||
}
|
||||
});
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
export function GetBoxArr(arr: Array<Object3D>): Box3
|
||||
{
|
||||
let box = new Box3();
|
||||
for (let o of arr)
|
||||
{
|
||||
let b = GetBox(o);
|
||||
if (!b.isEmpty())
|
||||
box.union(b);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
export function MoveMatrix(v: Vector3): Matrix4
|
||||
{
|
||||
let mat = new Matrix4();
|
||||
mat.makeTranslation(v.x, v.y, v.z);
|
||||
return mat;
|
||||
}
|
||||
|
||||
export function getProjectDist(v1: Vector3, v2: Vector3)
|
||||
{
|
||||
let ang = v1.angleTo(v2);
|
||||
let dist = v1.length();
|
||||
return {
|
||||
h: dist * Math.cos(ang),
|
||||
v: dist * Math.sin(ang)
|
||||
};
|
||||
}
|
||||
//获得输入点在2线组成的4个区间的位置
|
||||
export function getPtPostion(sp: Vector3, ep: Vector3, c: Vector3, inPt: Vector3)
|
||||
{
|
||||
let l1 = sp.clone().sub(c);
|
||||
let l2 = ep.clone().sub(c);
|
||||
let l3 = l1.clone().negate();
|
||||
let l4 = l2.clone().negate();
|
||||
let inputLine = inPt.clone().sub(c);
|
||||
let ang1 = angleTo(l1, l2);
|
||||
let ang2 = Math.PI;
|
||||
let ang3 = ang2 + Math.abs(ang1);
|
||||
let inputAng = angleTo(l1, inputLine);
|
||||
if (ang1 * inputAng < 0)
|
||||
{
|
||||
inputAng = (Math.PI * 2 - Math.abs(inputAng));
|
||||
}
|
||||
ang1 = Math.abs(ang1);
|
||||
inputAng = Math.abs(inputAng);
|
||||
if (inputAng <= ang1)
|
||||
{
|
||||
return { sp, ep };
|
||||
} else if (inputAng > ang1 && inputAng <= ang2)
|
||||
{
|
||||
return { sp: c.clone().add(l3), ep };
|
||||
} else if (inputAng > ang2 && inputAng <= ang3)
|
||||
{
|
||||
return { sp: c.clone().add(l3), ep: c.clone().add(l4) };
|
||||
} else
|
||||
{
|
||||
return { sp, ep: c.clone().add(l4) };
|
||||
}
|
||||
}
|
||||
export function angleAndX(v: Vector3 | Vector2)
|
||||
{
|
||||
return v.x ? Math.atan(v.y / v.x) : Math.PI / 2;
|
||||
}
|
||||
/**
|
||||
* 将角度调整为0-2pi之间
|
||||
*
|
||||
* @export
|
||||
* @param {number} an
|
||||
*/
|
||||
export function angleTo2Pi(an: number)
|
||||
{
|
||||
an = an % (Math.PI * 2);
|
||||
if (an < 0) an += Math.PI * 2;
|
||||
return an;
|
||||
}
|
||||
export function updateGeometry(l: Line | Mesh, geometry: Geometry)
|
||||
{
|
||||
let geo = l.geometry as Geometry;
|
||||
geo.dispose();
|
||||
l.geometry = geometry;
|
||||
geometry.verticesNeedUpdate = true;
|
||||
geometry.computeBoundingSphere();
|
||||
}
|
164
src/common/KeyEnum.ts
Normal file
164
src/common/KeyEnum.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
//鼠标类型
|
||||
export enum MouseKey
|
||||
{
|
||||
Left = 0,
|
||||
Middle = 1,
|
||||
Right = 2,
|
||||
}
|
||||
|
||||
export enum KeyBoard
|
||||
{
|
||||
// 数字
|
||||
Digit1 = 49,
|
||||
Digit2 = 50,
|
||||
Digit3 = 51,
|
||||
Digit4 = 52,
|
||||
Digit5 = 53,
|
||||
Digit6 = 54,
|
||||
Digit7 = 55,
|
||||
Digit8 = 56,
|
||||
Digit9 = 57,
|
||||
Digit0 = 58,
|
||||
// 字母
|
||||
KeyA = 65,
|
||||
KeyB = 66,
|
||||
KeyC = 67,
|
||||
KeyD = 68,
|
||||
KeyE = 69,
|
||||
KeyF = 70,
|
||||
KeyG = 71,
|
||||
KeyH = 72,
|
||||
KeyI = 73,
|
||||
KeyJ = 74,
|
||||
KeyK = 75,
|
||||
KeyL = 76,
|
||||
KeyM = 77,
|
||||
KeyN = 78,
|
||||
KeyO = 79,
|
||||
KeyP = 80,
|
||||
KeyQ = 81,
|
||||
KeyR = 82,
|
||||
KeyS = 83,
|
||||
KeyT = 84,
|
||||
KeyU = 85,
|
||||
KeyV = 86,
|
||||
KeyW = 87,
|
||||
KeyX = 88,
|
||||
KeyY = 89,
|
||||
KeyZ = 90,
|
||||
|
||||
// 符号
|
||||
/**
|
||||
* 逗号
|
||||
*/
|
||||
Comma = 188,
|
||||
CommaChrome = 229,
|
||||
/**
|
||||
* 句号
|
||||
*/
|
||||
Period = 190,
|
||||
/**
|
||||
* 分号
|
||||
*/
|
||||
Semicolon = 186,
|
||||
/**
|
||||
* 引号
|
||||
*/
|
||||
Quote = 222,
|
||||
/**
|
||||
* 左括号
|
||||
*/
|
||||
BracketLeft = 219,
|
||||
/**
|
||||
* 右括号
|
||||
*/
|
||||
BracketRight = 220,
|
||||
/**
|
||||
* 反引号
|
||||
*/
|
||||
Backquote = 192,
|
||||
/**
|
||||
* 反斜杠
|
||||
*/
|
||||
Backslash = 220,
|
||||
/**
|
||||
* 减号
|
||||
*/
|
||||
Minus = 189,
|
||||
/**
|
||||
* 等号
|
||||
*/
|
||||
Equal = 187,
|
||||
IntlRo = 193,
|
||||
IntlYen = 255,
|
||||
// 功能键
|
||||
Alt = 18,
|
||||
/**
|
||||
* 大写锁定
|
||||
*/
|
||||
CapsLock = 20,
|
||||
Control = 17,
|
||||
/**
|
||||
* win左键
|
||||
*/
|
||||
OSLeft = 91,
|
||||
/**
|
||||
* win右键
|
||||
*/
|
||||
OSRight = 92,
|
||||
Shift = 16,
|
||||
|
||||
ContextMenu = 93,
|
||||
Enter = 13,
|
||||
Space = 32,
|
||||
Backspace = 8,
|
||||
Tab = 9,
|
||||
Delete = 46,
|
||||
End = 35,
|
||||
Home = 36,
|
||||
Insert = 45,
|
||||
PageDown = 34,
|
||||
PageUp = 33,
|
||||
ArrowDown = 40,
|
||||
ArrowLeft = 37,
|
||||
ArrowRight = 39,
|
||||
ArrowUp = 38,
|
||||
Escape = 27,
|
||||
PrintScreen = 44,
|
||||
ScrollLock = 145,
|
||||
Pause = 19,
|
||||
|
||||
// F数字
|
||||
F1 = 112,
|
||||
F2 = 113,
|
||||
F3 = 114,
|
||||
F5 = 116,
|
||||
F6 = 117,
|
||||
F7 = 118,
|
||||
F8 = 119,
|
||||
F9 = 120,
|
||||
F10 = 121,
|
||||
F11 = 122,
|
||||
F12 = 123,
|
||||
|
||||
//数字键盘
|
||||
NumLock = 114,
|
||||
Numpad0 = 96,
|
||||
Numpad1 = 97,
|
||||
Numpad2 = 98,
|
||||
Numpad3 = 99,
|
||||
Numpad4 = 100,
|
||||
Numpad5 = 101,
|
||||
Numpad6 = 102,
|
||||
Numpad7 = 103,
|
||||
Numpad8 = 104,
|
||||
Numpad9 = 105,
|
||||
NumpadAdd = 107,
|
||||
NumpadDivide = 111,
|
||||
NumpadEqual = 12,
|
||||
NumpadMultiply = 106,
|
||||
NumpadSubtract = 109,
|
||||
NumpadDot = 110,
|
||||
NumpadDot1 = 190
|
||||
|
||||
}
|
236
src/common/MaterialEditor.ts
Normal file
236
src/common/MaterialEditor.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
|
||||
import { AmbientLight, BoxBufferGeometry, BufferGeometry, ConeBufferGeometry, CubeRefractionMapping, CubeTextureLoader, Geometry, Mesh, MeshPhysicalMaterial, Object3D, SphereBufferGeometry, sRGBEncoding, Texture, TorusBufferGeometry, TorusKnotBufferGeometry } from 'three';
|
||||
import { Singleton } from './Singleton';
|
||||
import { Viewer } from './Viewer';
|
||||
import { PMREMGenerator3 } from './PMREMGenerator2';
|
||||
import type { PhysicalMaterialRecord, TextureTableRecord } from 'webcad_ue4_api';
|
||||
import { MaterialEditorCamerControl } from './MaterialMouseControl';
|
||||
import { ref } from 'vue';
|
||||
|
||||
async function textureRenderUpdate(textureRecord:TextureTableRecord){
|
||||
const texture = textureRecord['texture'] as Texture;
|
||||
|
||||
texture.wrapS = textureRecord.WrapS;
|
||||
texture.wrapT = textureRecord.WrapT;
|
||||
texture.anisotropy = 16;
|
||||
texture.rotation = textureRecord.rotation;
|
||||
texture.repeat.set(textureRecord.repeatX, textureRecord.repeatY);
|
||||
texture.offset.set(textureRecord.moveX, textureRecord.moveY);
|
||||
texture.needsUpdate = true;
|
||||
|
||||
// for (let f of this.waits)
|
||||
// f();
|
||||
// this.waits.length = 0;
|
||||
//
|
||||
// this.AsyncUpdated();
|
||||
}
|
||||
/**
|
||||
* 材质编辑器
|
||||
*/
|
||||
export class MaterialEditor extends Singleton
|
||||
{
|
||||
Geometrys: Map<string, Geometry | BufferGeometry>;
|
||||
|
||||
CurGeometryName = ref("球");
|
||||
Canvas: HTMLElement;
|
||||
ShowObject: Object3D;
|
||||
ShowMesh: Mesh;
|
||||
Viewer: Viewer;
|
||||
|
||||
private _MeshMaterial: MeshPhysicalMaterial = new MeshPhysicalMaterial({});
|
||||
//构造
|
||||
private constructor()
|
||||
{
|
||||
super();
|
||||
this.initGeometrys();
|
||||
|
||||
this.LoadDefaultExr();
|
||||
this.LoadMetalEnv();
|
||||
}
|
||||
|
||||
initGeometrys()
|
||||
{
|
||||
this.Geometrys = new Map<string, Geometry | BufferGeometry>(
|
||||
[
|
||||
["球", new SphereBufferGeometry(1000, 32, 32)],
|
||||
["圆环", new TorusBufferGeometry(0.8 * 1e3, 0.4 * 1e3, 32, 64)],
|
||||
["立方体", new BoxBufferGeometry(1e3, 1e3, 1e3, 1, 1, 1)],
|
||||
["环面纽结", new TorusKnotBufferGeometry(0.7 * 1e3, 0.3 * 1e3, 128, 64)],
|
||||
["圆锥体", new ConeBufferGeometry(1 * 1e3, 2 * 1e3, 32)],
|
||||
// ["球(多面)", new SphereBufferGeometry(1 * 1e3, 64, 64)],
|
||||
// ["立方体(多面)", new BoxBufferGeometry(1 * 1e3, 1 * 1e3, 1 * 1e3, 128, 128, 128)]
|
||||
]
|
||||
);
|
||||
}
|
||||
initViewer()
|
||||
{
|
||||
if (!this.Viewer)
|
||||
{
|
||||
this.Viewer = new Viewer(this.Canvas);
|
||||
// this.Viewer.PreViewer.Cursor.CursorObject.visible = false;
|
||||
// this.Viewer.CameraCtrl.CameraType = CameraType.PerspectiveCamera;
|
||||
// this.Viewer.UsePass = false;
|
||||
this.initScene();
|
||||
new MaterialEditorCamerControl(this.Viewer);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Canvas.appendChild(this.Viewer.Renderer.domElement);
|
||||
}
|
||||
|
||||
}
|
||||
SetViewer(canvas: HTMLElement)
|
||||
{
|
||||
this.Canvas = canvas;
|
||||
this.initViewer();
|
||||
this.CurGeometryName.value = "球";
|
||||
}
|
||||
initScene()
|
||||
{
|
||||
let scene = this.Viewer.Scene;
|
||||
this.ShowObject = new Object3D();
|
||||
let geo = this.Geometrys.get(this.CurGeometryName.value);
|
||||
this.ShowMesh = new Mesh(geo, this._MeshMaterial);
|
||||
this.ShowMesh.scale.set(1000, 1000, 1000);
|
||||
|
||||
this.ShowObject.add(this.ShowMesh);
|
||||
scene.add(this.ShowObject);
|
||||
// let remove = autorun(() =>
|
||||
// {
|
||||
// let geo = this.Geometrys.get(this.CurGeometryName.get());
|
||||
// if (geo)
|
||||
// {
|
||||
// this.ShowMesh.geometry = geo;
|
||||
// this.Viewer.UpdateRender();
|
||||
// }
|
||||
// });
|
||||
// end(this as MaterialEditor, this.dispose, remove);
|
||||
|
||||
//环境光
|
||||
let ambient = new AmbientLight();
|
||||
ambient.intensity = 0.7;
|
||||
scene.add(ambient);
|
||||
}
|
||||
|
||||
metaTexture: Texture;
|
||||
metaPromise: Promise<Texture>;
|
||||
async LoadMetalEnv(): Promise<Texture>
|
||||
{
|
||||
if (this.metaTexture) return this.metaTexture;
|
||||
if (this.metaPromise) return this.metaPromise;
|
||||
|
||||
return new Promise((res, rej) =>
|
||||
{
|
||||
let urls = ['right.webp', 'left.webp', 'top.webp', 'bottom.webp', 'front.webp', 'back.webp'];
|
||||
new CubeTextureLoader().setPath('https://cdn.cfcad.cn/t/house/')
|
||||
.load(urls, (t) =>
|
||||
{
|
||||
t.encoding = sRGBEncoding;
|
||||
t.mapping = CubeRefractionMapping;
|
||||
|
||||
let pmremGenerator = new PMREMGenerator3(this.Viewer.Renderer);
|
||||
let ldrCubeRenderTarget = pmremGenerator.fromCubemap(t);
|
||||
|
||||
this.metaTexture = ldrCubeRenderTarget.texture;
|
||||
res(this.metaTexture);
|
||||
|
||||
this.Viewer.Scene.background = this.metaTexture;
|
||||
// this.Viewer.Scene.background = new Color(255,0,0);
|
||||
|
||||
this.Viewer.UpdateRender();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exrPromise: Promise<Texture>;
|
||||
exrTexture: Texture;
|
||||
async LoadDefaultExr(): Promise<Texture>
|
||||
{
|
||||
if (this.exrTexture) return this.exrTexture;
|
||||
if (this.exrPromise) return this.exrPromise;
|
||||
|
||||
this.exrPromise = new Promise<Texture>((res, rej) =>
|
||||
{
|
||||
let urls = ['right.webp', 'left.webp', 'top.webp', 'bottom.webp', 'front.webp', 'back.webp'];
|
||||
new CubeTextureLoader().setPath('https://cdn.cfcad.cn/t/house_gray/')
|
||||
.load(urls, (t) =>
|
||||
{
|
||||
t.encoding = sRGBEncoding;
|
||||
t.mapping = CubeRefractionMapping;
|
||||
let pmremGenerator = new PMREMGenerator3(this.Viewer.Renderer);
|
||||
let target = pmremGenerator.fromCubemap(t);
|
||||
this.exrTexture = target.texture;
|
||||
res(this.exrTexture);
|
||||
|
||||
this.Viewer.UpdateRender();
|
||||
});
|
||||
});
|
||||
return this.exrPromise;
|
||||
}
|
||||
|
||||
private Material: PhysicalMaterialRecord;
|
||||
setMaterial(mat: PhysicalMaterialRecord)
|
||||
{
|
||||
this.Material = mat;
|
||||
this._MeshMaterial.copy(mat.Material);
|
||||
|
||||
let mtl = this._MeshMaterial;
|
||||
if (mtl.metalness > 0.8)
|
||||
this.LoadMetalEnv().then(env =>
|
||||
{
|
||||
mtl.envMap = env;
|
||||
mtl.needsUpdate = true;
|
||||
});
|
||||
else
|
||||
this.LoadDefaultExr().then(exr =>
|
||||
{
|
||||
mtl.envMap = exr;
|
||||
mtl.needsUpdate = true;
|
||||
});
|
||||
|
||||
this.Update();
|
||||
}
|
||||
|
||||
dispose()
|
||||
{
|
||||
}
|
||||
|
||||
async Update()
|
||||
{
|
||||
let mat = this.ShowMesh.material as MeshPhysicalMaterial;
|
||||
if (this.Material.map && this.Material.map.Object && this.Material.useMap)
|
||||
{
|
||||
let texture = this.Material.map.Object as TextureTableRecord;
|
||||
await textureRenderUpdate(texture);
|
||||
}
|
||||
if (this.Material.bumpMap && this.Material.bumpMap.Object)
|
||||
{
|
||||
let texture = this.Material.bumpMap.Object as TextureTableRecord;
|
||||
await textureRenderUpdate(texture);
|
||||
}
|
||||
if (this.Material.roughnessMap && this.Material.roughnessMap.Object)
|
||||
{
|
||||
let texture = this.Material.roughnessMap.Object as TextureTableRecord;
|
||||
await textureRenderUpdate(texture);
|
||||
}
|
||||
mat.needsUpdate = true;
|
||||
|
||||
this._MeshMaterial.copy(this.Material.Material);
|
||||
|
||||
let mtl = this._MeshMaterial;
|
||||
if (mtl.metalness > 0.8)
|
||||
this.LoadMetalEnv().then(env =>
|
||||
{
|
||||
mtl.envMap = env;
|
||||
mtl.needsUpdate = true;
|
||||
});
|
||||
else
|
||||
this.LoadDefaultExr().then(exr =>
|
||||
{
|
||||
mtl.envMap = exr;
|
||||
mtl.needsUpdate = true;
|
||||
});
|
||||
|
||||
this.Viewer.UpdateRender();
|
||||
}
|
||||
}
|
85
src/common/MaterialMouseControl.ts
Normal file
85
src/common/MaterialMouseControl.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Vector3 } from 'three';
|
||||
import type { Viewer } from './Viewer';
|
||||
|
||||
/**
|
||||
* 材质编辑器的场景鼠标控制.
|
||||
*/
|
||||
export class MaterialEditorCamerControl
|
||||
{
|
||||
private Viewer: Viewer;
|
||||
|
||||
//State.
|
||||
private _MouseIsDown: boolean = false;
|
||||
private _StartPoint: Vector3 = new Vector3();
|
||||
private _EndPoint = new Vector3();
|
||||
private pointId: number;
|
||||
|
||||
constructor(view: Viewer)
|
||||
{
|
||||
this.Viewer = view;
|
||||
|
||||
this.initMouseControl();
|
||||
}
|
||||
initMouseControl()
|
||||
{
|
||||
let el = this.Viewer.Renderer.domElement;
|
||||
el.addEventListener("pointerdown", (e) => { this.pointId = e.pointerId; }, false);
|
||||
el.addEventListener("mousedown", this.onMouseDown, false);
|
||||
el.addEventListener("mousemove", this.onMouseMove, false);
|
||||
el.addEventListener("mouseup", this.onMouseUp, false);
|
||||
el.addEventListener('wheel', this.onMouseWheel, false);
|
||||
}
|
||||
onMouseDown = (event: MouseEvent) =>
|
||||
{
|
||||
this.requestPointerLock();
|
||||
this._MouseIsDown = true;
|
||||
this._StartPoint.set(event.offsetX, event.offsetY, 0);
|
||||
};
|
||||
onMouseUp = (event: MouseEvent) =>
|
||||
{
|
||||
this._MouseIsDown = false;
|
||||
this.exitPointerLock();
|
||||
};
|
||||
onMouseMove = (event: MouseEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
if (this._MouseIsDown)
|
||||
{
|
||||
this._EndPoint.set(event.offsetX, event.offsetY, 0);
|
||||
let changeVec: Vector3 = new Vector3();
|
||||
changeVec.subVectors(this._EndPoint, this._StartPoint);
|
||||
this._StartPoint.copy(this._EndPoint);
|
||||
|
||||
this.Viewer.Rotate(changeVec);
|
||||
}
|
||||
};
|
||||
onMouseWheel = (event: WheelEvent) =>
|
||||
{
|
||||
if (event.deltaY < 0)
|
||||
{
|
||||
this.Viewer.Zoom(0.6);
|
||||
}
|
||||
else if (event.deltaY > 0)
|
||||
{
|
||||
this.Viewer.Zoom(1.4);
|
||||
}
|
||||
};
|
||||
|
||||
requestPointerLock()
|
||||
{
|
||||
if (this.Viewer.Renderer.domElement.setPointerCapture)
|
||||
this.Viewer.Renderer.domElement.setPointerCapture(this.pointId);
|
||||
}
|
||||
|
||||
exitPointerLock()
|
||||
{
|
||||
if (this.Viewer.Renderer.domElement.releasePointerCapture && this.pointId !== undefined)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Viewer.Renderer.domElement.releasePointerCapture(this.pointId);
|
||||
}
|
||||
catch (error) { }
|
||||
}
|
||||
}
|
||||
}
|
33
src/common/Matrix2.ts
Normal file
33
src/common/Matrix2.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Vector2, Vector3 } from "three";
|
||||
|
||||
export class Matrix2
|
||||
{
|
||||
private el = [1, 0, 0, 1];
|
||||
set(n11: number, n12: number, n21: number, n22: number)
|
||||
{
|
||||
let te = this.el;
|
||||
|
||||
te[0] = n11; te[1] = n21;
|
||||
te[2] = n12; te[3] = n22;
|
||||
return this;
|
||||
}
|
||||
applyVector(vec: Vector2 | Vector3)
|
||||
{
|
||||
let x = vec.x, y = vec.y;
|
||||
let e = this.el;
|
||||
vec.x = e[0] * x + e[2] * y;
|
||||
vec.y = e[1] * x + e[3] * y;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setRotate(theta: number): Matrix2
|
||||
{
|
||||
// let el = this.el;
|
||||
let c = Math.cos(theta), s = Math.sin(theta);
|
||||
|
||||
this.set(c, -s,
|
||||
s, c);
|
||||
return this;
|
||||
}
|
||||
}
|
997
src/common/PMREMGenerator2.ts
Normal file
997
src/common/PMREMGenerator2.ts
Normal file
@@ -0,0 +1,997 @@
|
||||
import { BufferAttribute, BufferGeometry, CubeUVReflectionMapping, GammaEncoding, LinearEncoding, Mesh, NearestFilter, NoBlending, NoToneMapping, OrthographicCamera, PerspectiveCamera, RawShaderMaterial, RGBDEncoding, RGBEEncoding, RGBEFormat, RGBM16Encoding, RGBM7Encoding, sRGBEncoding, UnsignedByteType, Vector2, Vector3, WebGLRenderer, WebGLRenderTarget } from "three";
|
||||
|
||||
const LOD_MIN = 4;
|
||||
const LOD_MAX = 8;
|
||||
const SIZE_MAX = Math.pow(2, LOD_MAX);
|
||||
|
||||
// The standard deviations (radians) associated with the extra mips. These are
|
||||
// chosen to approximate a Trowbridge-Reitz distribution function times the
|
||||
// geometric shadowing function. These sigma values squared must match the
|
||||
// variance #defines in cube_uv_reflection_fragment.glsl.js.
|
||||
const EXTRA_LOD_SIGMA = [0.125, 0.215, 0.35, 0.446, 0.526, 0.582];
|
||||
|
||||
const TOTAL_LODS = LOD_MAX - LOD_MIN + 1 + EXTRA_LOD_SIGMA.length;
|
||||
|
||||
// The maximum length of the blur for loop. Smaller sigmas will use fewer
|
||||
// samples and exit early, but not recompile the shader.
|
||||
const MAX_SAMPLES = 20;
|
||||
|
||||
const ENCODINGS = {
|
||||
[LinearEncoding]: 0,
|
||||
[sRGBEncoding]: 1,
|
||||
[RGBEEncoding]: 2,
|
||||
[RGBM7Encoding]: 3,
|
||||
[RGBM16Encoding]: 4,
|
||||
[RGBDEncoding]: 5,
|
||||
[GammaEncoding]: 6
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
const _flatCamera = /*@__PURE__*/ new OrthographicCamera();
|
||||
const { _lodPlanes, _sizeLods, _sigmas } = /*@__PURE__*/ _createPlanes();
|
||||
let _oldTarget = null;
|
||||
|
||||
// Golden Ratio
|
||||
const PHI = (1 + Math.sqrt(5)) / 2;
|
||||
const INV_PHI = 1 / PHI;
|
||||
|
||||
// Vertices of a dodecahedron (except the opposites, which represent the
|
||||
// same axis), used as axis directions evenly spread on a sphere.
|
||||
const _axisDirections = [
|
||||
/*@__PURE__*/ new Vector3(1, 1, 1),
|
||||
/*@__PURE__*/ new Vector3(- 1, 1, 1),
|
||||
/*@__PURE__*/ new Vector3(1, 1, - 1),
|
||||
/*@__PURE__*/ new Vector3(- 1, 1, - 1),
|
||||
/*@__PURE__*/ new Vector3(0, PHI, INV_PHI),
|
||||
/*@__PURE__*/ new Vector3(0, PHI, - INV_PHI),
|
||||
/*@__PURE__*/ new Vector3(INV_PHI, 0, PHI),
|
||||
/*@__PURE__*/ new Vector3(- INV_PHI, 0, PHI),
|
||||
/*@__PURE__*/ new Vector3(PHI, INV_PHI, 0),
|
||||
/*@__PURE__*/ new Vector3(- PHI, INV_PHI, 0)];
|
||||
|
||||
/**
|
||||
* This class generates a Prefiltered, Mipmapped Radiance Environment Map
|
||||
* (PMREM) from a cubeMap environment texture. This allows different levels of
|
||||
* blur to be quickly accessed based on material roughness. It is packed into a
|
||||
* special CubeUV format that allows us to perform custom interpolation so that
|
||||
* we can support nonlinear formats such as RGBE. Unlike a traditional mipmap
|
||||
* chain, it only goes down to the LOD_MIN level (above), and then creates extra
|
||||
* even more filtered 'mips' at the same LOD_MIN resolution, associated with
|
||||
* higher roughness levels. In this way we maintain resolution to smoothly
|
||||
* interpolate diffuse lighting while limiting sampling computation.
|
||||
*/
|
||||
|
||||
export class PMREMGenerator3
|
||||
{
|
||||
_renderer: WebGLRenderer;
|
||||
_pingPongRenderTarget: any;
|
||||
_blurMaterial: RawShaderMaterial;
|
||||
_equirectShader: any;
|
||||
_cubemapShader: any;
|
||||
|
||||
constructor(renderer: WebGLRenderer)
|
||||
{
|
||||
|
||||
this._renderer = renderer;
|
||||
this._pingPongRenderTarget = null;
|
||||
|
||||
this._blurMaterial = _getBlurShader(MAX_SAMPLES);
|
||||
this._equirectShader = null;
|
||||
this._cubemapShader = null;
|
||||
|
||||
this._compileMaterial(this._blurMaterial);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PMREM from a supplied Scene, which can be faster than using an
|
||||
* image if networking bandwidth is low. Optional sigma specifies a blur radius
|
||||
* in radians to be applied to the scene before PMREM generation. Optional near
|
||||
* and far planes ensure the scene is rendered in its entirety (the cubeCamera
|
||||
* is placed at the origin).
|
||||
*/
|
||||
fromScene(scene, sigma = 0, near = 0.1, far = 100)
|
||||
{
|
||||
|
||||
_oldTarget = this._renderer.getRenderTarget();
|
||||
const cubeUVRenderTarget = this._allocateTargets();
|
||||
|
||||
this._sceneToCubeUV(scene, near, far, cubeUVRenderTarget);
|
||||
if (sigma > 0)
|
||||
{
|
||||
|
||||
this._blur(cubeUVRenderTarget, 0, 0, sigma);
|
||||
|
||||
}
|
||||
|
||||
this._applyPMREM(cubeUVRenderTarget);
|
||||
this._cleanup(cubeUVRenderTarget);
|
||||
|
||||
return cubeUVRenderTarget;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PMREM from an equirectangular texture, which can be either LDR
|
||||
* (RGBFormat) or HDR (RGBEFormat). The ideal input image size is 1k (1024 x 512),
|
||||
* as this matches best with the 256 x 256 cubemap output.
|
||||
*/
|
||||
fromEquirectangular(equirectangular)
|
||||
{
|
||||
|
||||
return this._fromTexture(equirectangular);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PMREM from an cubemap texture, which can be either LDR
|
||||
* (RGBFormat) or HDR (RGBEFormat). The ideal input cube size is 256 x 256,
|
||||
* as this matches best with the 256 x 256 cubemap output.
|
||||
*/
|
||||
fromCubemap(cubemap)
|
||||
{
|
||||
|
||||
return this._fromTexture(cubemap);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-compiles the cubemap shader. You can get faster start-up by invoking this method during
|
||||
* your texture's network fetch for increased concurrency.
|
||||
*/
|
||||
compileCubemapShader()
|
||||
{
|
||||
|
||||
if (this._cubemapShader === null)
|
||||
{
|
||||
|
||||
this._cubemapShader = _getCubemapShader();
|
||||
this._compileMaterial(this._cubemapShader);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-compiles the equirectangular shader. You can get faster start-up by invoking this method during
|
||||
* your texture's network fetch for increased concurrency.
|
||||
*/
|
||||
compileEquirectangularShader()
|
||||
{
|
||||
|
||||
if (this._equirectShader === null)
|
||||
{
|
||||
|
||||
this._equirectShader = _getEquirectShader();
|
||||
this._compileMaterial(this._equirectShader);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of the PMREMGenerator's internal memory. Note that PMREMGenerator is a static class,
|
||||
* so you should not need more than one PMREMGenerator object. If you do, calling dispose() on
|
||||
* one of them will cause any others to also become unusable.
|
||||
*/
|
||||
dispose()
|
||||
{
|
||||
|
||||
this._blurMaterial.dispose();
|
||||
|
||||
if (this._cubemapShader !== null) this._cubemapShader.dispose();
|
||||
if (this._equirectShader !== null) this._equirectShader.dispose();
|
||||
|
||||
for (let i = 0; i < _lodPlanes.length; i++)
|
||||
{
|
||||
|
||||
_lodPlanes[i].dispose();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// private interface
|
||||
|
||||
_cleanup(outputTarget)
|
||||
{
|
||||
|
||||
this._pingPongRenderTarget.dispose();
|
||||
this._renderer.setRenderTarget(_oldTarget);
|
||||
outputTarget.scissorTest = false;
|
||||
_setViewport(outputTarget, 0, 0, outputTarget.width, outputTarget.height);
|
||||
|
||||
}
|
||||
|
||||
_fromTexture(texture)
|
||||
{
|
||||
|
||||
_oldTarget = this._renderer.getRenderTarget();
|
||||
const cubeUVRenderTarget = this._allocateTargets(texture);
|
||||
this._textureToCubeUV(texture, cubeUVRenderTarget);
|
||||
this._applyPMREM(cubeUVRenderTarget);
|
||||
this._cleanup(cubeUVRenderTarget);
|
||||
|
||||
return cubeUVRenderTarget;
|
||||
|
||||
}
|
||||
|
||||
_allocateTargets(texture?)
|
||||
{ // warning: null texture is valid
|
||||
|
||||
const params = {
|
||||
magFilter: NearestFilter,
|
||||
minFilter: NearestFilter,
|
||||
generateMipmaps: false,
|
||||
type: UnsignedByteType,
|
||||
format: RGBEFormat,
|
||||
encoding: _isLDR(texture) ? texture.encoding : RGBEEncoding,
|
||||
depthBuffer: false
|
||||
};
|
||||
|
||||
const cubeUVRenderTarget = _createRenderTarget(params);
|
||||
cubeUVRenderTarget.depthBuffer = texture ? false : true;
|
||||
this._pingPongRenderTarget = _createRenderTarget(params);
|
||||
return cubeUVRenderTarget;
|
||||
|
||||
}
|
||||
|
||||
_compileMaterial(material)
|
||||
{
|
||||
|
||||
const tmpMesh = new Mesh(_lodPlanes[0], material);
|
||||
this._renderer.compile(tmpMesh, _flatCamera);
|
||||
|
||||
}
|
||||
|
||||
_sceneToCubeUV(scene, near, far, cubeUVRenderTarget)
|
||||
{
|
||||
|
||||
const fov = 90;
|
||||
const aspect = 1;
|
||||
const cubeCamera = new PerspectiveCamera(fov, aspect, near, far);
|
||||
const upSign = [1, - 1, 1, 1, 1, 1];
|
||||
const forwardSign = [1, 1, 1, - 1, - 1, - 1];
|
||||
const renderer = this._renderer;
|
||||
|
||||
const outputEncoding = renderer.outputEncoding;
|
||||
const toneMapping = renderer.toneMapping;
|
||||
const clearColor = renderer.getClearColor();
|
||||
const clearAlpha = renderer.getClearAlpha();
|
||||
|
||||
renderer.toneMapping = NoToneMapping;
|
||||
renderer.outputEncoding = LinearEncoding;
|
||||
|
||||
let background = scene.background;
|
||||
if (background && background.isColor)
|
||||
{
|
||||
|
||||
background.convertSRGBToLinear();
|
||||
// Convert linear to RGBE
|
||||
const maxComponent = Math.max(background.r, background.g, background.b);
|
||||
const fExp = Math.min(Math.max(Math.ceil(Math.log2(maxComponent)), - 128.0), 127.0);
|
||||
background = background.multiplyScalar(Math.pow(2.0, - fExp));
|
||||
const alpha = (fExp + 128.0) / 255.0;
|
||||
renderer.setClearColor(background, alpha);
|
||||
scene.background = null;
|
||||
|
||||
}
|
||||
|
||||
for (let i = 0; i < 6; i++)
|
||||
{
|
||||
|
||||
const col = i % 3;
|
||||
if (col == 0)
|
||||
{
|
||||
|
||||
cubeCamera.up.set(0, upSign[i], 0);
|
||||
cubeCamera.lookAt(forwardSign[i], 0, 0);
|
||||
|
||||
} else if (col == 1)
|
||||
{
|
||||
|
||||
cubeCamera.up.set(0, 0, upSign[i]);
|
||||
cubeCamera.lookAt(0, forwardSign[i], 0);
|
||||
|
||||
} else
|
||||
{
|
||||
|
||||
cubeCamera.up.set(0, upSign[i], 0);
|
||||
cubeCamera.lookAt(0, 0, forwardSign[i]);
|
||||
|
||||
}
|
||||
|
||||
_setViewport(cubeUVRenderTarget,
|
||||
col * SIZE_MAX, i > 2 ? SIZE_MAX : 0, SIZE_MAX, SIZE_MAX);
|
||||
renderer.setRenderTarget(cubeUVRenderTarget);
|
||||
renderer.render(scene, cubeCamera);
|
||||
|
||||
}
|
||||
|
||||
renderer.toneMapping = toneMapping;
|
||||
renderer.outputEncoding = outputEncoding;
|
||||
renderer.setClearColor(clearColor, clearAlpha);
|
||||
|
||||
}
|
||||
|
||||
_textureToCubeUV(texture, cubeUVRenderTarget)
|
||||
{
|
||||
|
||||
const renderer = this._renderer;
|
||||
|
||||
if (texture.isCubeTexture)
|
||||
{
|
||||
|
||||
if (this._cubemapShader == null)
|
||||
{
|
||||
|
||||
this._cubemapShader = _getCubemapShader();
|
||||
|
||||
}
|
||||
|
||||
} else
|
||||
{
|
||||
|
||||
if (this._equirectShader == null)
|
||||
{
|
||||
|
||||
this._equirectShader = _getEquirectShader();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const material = texture.isCubeTexture ? this._cubemapShader : this._equirectShader;
|
||||
const mesh = new Mesh(_lodPlanes[0], material);
|
||||
|
||||
const uniforms = material.uniforms;
|
||||
|
||||
uniforms['envMap'].value = texture;
|
||||
|
||||
if (!texture.isCubeTexture)
|
||||
{
|
||||
|
||||
uniforms['texelSize'].value.set(1.0 / texture.image.width, 1.0 / texture.image.height);
|
||||
|
||||
}
|
||||
|
||||
uniforms['inputEncoding'].value = ENCODINGS[texture.encoding];
|
||||
uniforms['outputEncoding'].value = ENCODINGS[cubeUVRenderTarget.texture.encoding];
|
||||
|
||||
_setViewport(cubeUVRenderTarget, 0, 0, 3 * SIZE_MAX, 2 * SIZE_MAX);
|
||||
|
||||
renderer.setRenderTarget(cubeUVRenderTarget);
|
||||
renderer.render(mesh, _flatCamera);
|
||||
|
||||
}
|
||||
|
||||
_applyPMREM(cubeUVRenderTarget)
|
||||
{
|
||||
|
||||
const renderer = this._renderer;
|
||||
const autoClear = renderer.autoClear;
|
||||
renderer.autoClear = false;
|
||||
|
||||
for (let i = 1; i < TOTAL_LODS; i++)
|
||||
{
|
||||
|
||||
const sigma = Math.sqrt(_sigmas[i] * _sigmas[i] - _sigmas[i - 1] * _sigmas[i - 1]);
|
||||
|
||||
const poleAxis = _axisDirections[(i - 1) % _axisDirections.length];
|
||||
|
||||
this._blur(cubeUVRenderTarget, i - 1, i, sigma, poleAxis);
|
||||
|
||||
}
|
||||
|
||||
renderer.autoClear = autoClear;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a two-pass Gaussian blur for a cubemap. Normally this is done
|
||||
* vertically and horizontally, but this breaks down on a cube. Here we apply
|
||||
* the blur latitudinally (around the poles), and then longitudinally (towards
|
||||
* the poles) to approximate the orthogonally-separable blur. It is least
|
||||
* accurate at the poles, but still does a decent job.
|
||||
*/
|
||||
_blur(cubeUVRenderTarget, lodIn, lodOut, sigma, poleAxis?)
|
||||
{
|
||||
|
||||
const pingPongRenderTarget = this._pingPongRenderTarget;
|
||||
|
||||
this._halfBlur(
|
||||
cubeUVRenderTarget,
|
||||
pingPongRenderTarget,
|
||||
lodIn,
|
||||
lodOut,
|
||||
sigma,
|
||||
'latitudinal',
|
||||
poleAxis);
|
||||
|
||||
this._halfBlur(
|
||||
pingPongRenderTarget,
|
||||
cubeUVRenderTarget,
|
||||
lodOut,
|
||||
lodOut,
|
||||
sigma,
|
||||
'longitudinal',
|
||||
poleAxis);
|
||||
|
||||
}
|
||||
|
||||
_halfBlur(targetIn, targetOut, lodIn, lodOut, sigmaRadians, direction, poleAxis)
|
||||
{
|
||||
|
||||
const renderer = this._renderer;
|
||||
const blurMaterial = this._blurMaterial;
|
||||
|
||||
if (direction !== 'latitudinal' && direction !== 'longitudinal')
|
||||
{
|
||||
|
||||
console.error(
|
||||
'blur direction must be either latitudinal or longitudinal!');
|
||||
|
||||
}
|
||||
|
||||
// Number of standard deviations at which to cut off the discrete approximation.
|
||||
const STANDARD_DEVIATIONS = 3;
|
||||
|
||||
const blurMesh = new Mesh(_lodPlanes[lodOut], blurMaterial);
|
||||
const blurUniforms = blurMaterial.uniforms;
|
||||
|
||||
const pixels = _sizeLods[lodIn] - 1;
|
||||
const radiansPerPixel = isFinite(sigmaRadians) ? Math.PI / (2 * pixels) : 2 * Math.PI / (2 * MAX_SAMPLES - 1);
|
||||
const sigmaPixels = sigmaRadians / radiansPerPixel;
|
||||
const samples = isFinite(sigmaRadians) ? 1 + Math.floor(STANDARD_DEVIATIONS * sigmaPixels) : MAX_SAMPLES;
|
||||
|
||||
if (samples > MAX_SAMPLES)
|
||||
{
|
||||
|
||||
console.warn(`sigmaRadians, ${sigmaRadians}, is too large and will clip, as it requested ${samples} samples when the maximum is set to ${MAX_SAMPLES}`);
|
||||
|
||||
}
|
||||
|
||||
const weights = [];
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < MAX_SAMPLES; ++i)
|
||||
{
|
||||
|
||||
const x = i / sigmaPixels;
|
||||
const weight = Math.exp(- x * x / 2);
|
||||
weights.push(weight);
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
|
||||
sum += weight;
|
||||
|
||||
} else if (i < samples)
|
||||
{
|
||||
|
||||
sum += 2 * weight;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (let i = 0; i < weights.length; i++)
|
||||
{
|
||||
|
||||
weights[i] = weights[i] / sum;
|
||||
|
||||
}
|
||||
|
||||
blurUniforms['envMap'].value = targetIn.texture;
|
||||
blurUniforms['samples'].value = samples;
|
||||
blurUniforms['weights'].value = weights;
|
||||
blurUniforms['latitudinal'].value = direction === 'latitudinal';
|
||||
|
||||
if (poleAxis)
|
||||
{
|
||||
|
||||
blurUniforms['poleAxis'].value = poleAxis;
|
||||
|
||||
}
|
||||
|
||||
blurUniforms['dTheta'].value = radiansPerPixel;
|
||||
blurUniforms['mipInt'].value = LOD_MAX - lodIn;
|
||||
blurUniforms['inputEncoding'].value = ENCODINGS[targetIn.texture.encoding];
|
||||
blurUniforms['outputEncoding'].value = ENCODINGS[targetIn.texture.encoding];
|
||||
|
||||
const outputSize = _sizeLods[lodOut];
|
||||
const x = 3 * Math.max(0, SIZE_MAX - 2 * outputSize);
|
||||
const y = (lodOut === 0 ? 0 : 2 * SIZE_MAX) + 2 * outputSize * (lodOut > LOD_MAX - LOD_MIN ? lodOut - LOD_MAX + LOD_MIN : 0);
|
||||
|
||||
_setViewport(targetOut, x, y, 3 * outputSize, 2 * outputSize);
|
||||
renderer.setRenderTarget(targetOut);
|
||||
renderer.render(blurMesh, _flatCamera);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function _isLDR(texture)
|
||||
{
|
||||
|
||||
if (texture === undefined || texture.type !== UnsignedByteType) return false;
|
||||
|
||||
return texture.encoding === LinearEncoding || texture.encoding === sRGBEncoding || texture.encoding === GammaEncoding;
|
||||
|
||||
}
|
||||
|
||||
function _createPlanes()
|
||||
{
|
||||
|
||||
const _lodPlanes = [];
|
||||
const _sizeLods = [];
|
||||
const _sigmas = [];
|
||||
|
||||
let lod = LOD_MAX;
|
||||
|
||||
for (let i = 0; i < TOTAL_LODS; i++)
|
||||
{
|
||||
|
||||
const sizeLod = Math.pow(2, lod);
|
||||
_sizeLods.push(sizeLod);
|
||||
let sigma = 1.0 / sizeLod;
|
||||
|
||||
if (i > LOD_MAX - LOD_MIN)
|
||||
{
|
||||
|
||||
sigma = EXTRA_LOD_SIGMA[i - LOD_MAX + LOD_MIN - 1];
|
||||
|
||||
} else if (i == 0)
|
||||
{
|
||||
|
||||
sigma = 0;
|
||||
|
||||
}
|
||||
|
||||
_sigmas.push(sigma);
|
||||
|
||||
const texelSize = 1.0 / (sizeLod - 1);
|
||||
const min = - texelSize / 2;
|
||||
const max = 1 + texelSize / 2;
|
||||
const uv1 = [min, min, max, min, max, max, min, min, max, max, min, max];
|
||||
|
||||
const cubeFaces = 6;
|
||||
const vertices = 6;
|
||||
const positionSize = 3;
|
||||
const uvSize = 2;
|
||||
const faceIndexSize = 1;
|
||||
|
||||
const position = new Float32Array(positionSize * vertices * cubeFaces);
|
||||
const uv = new Float32Array(uvSize * vertices * cubeFaces);
|
||||
const faceIndex = new Float32Array(faceIndexSize * vertices * cubeFaces);
|
||||
|
||||
for (let face = 0; face < cubeFaces; face++)
|
||||
{
|
||||
|
||||
const x = (face % 3) * 2 / 3 - 1;
|
||||
const y = face > 2 ? 0 : - 1;
|
||||
const coordinates = [
|
||||
x, y, 0,
|
||||
x + 2 / 3, y, 0,
|
||||
x + 2 / 3, y + 1, 0,
|
||||
x, y, 0,
|
||||
x + 2 / 3, y + 1, 0,
|
||||
x, y + 1, 0
|
||||
];
|
||||
position.set(coordinates, positionSize * vertices * face);
|
||||
uv.set(uv1, uvSize * vertices * face);
|
||||
const fill = [face, face, face, face, face, face];
|
||||
faceIndex.set(fill, faceIndexSize * vertices * face);
|
||||
|
||||
}
|
||||
|
||||
const planes = new BufferGeometry();
|
||||
planes.setAttribute('position', new BufferAttribute(position, positionSize));
|
||||
planes.setAttribute('uv', new BufferAttribute(uv, uvSize));
|
||||
planes.setAttribute('faceIndex', new BufferAttribute(faceIndex, faceIndexSize));
|
||||
_lodPlanes.push(planes);
|
||||
|
||||
if (lod > LOD_MIN)
|
||||
{
|
||||
|
||||
lod--;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return { _lodPlanes, _sizeLods, _sigmas };
|
||||
|
||||
}
|
||||
|
||||
function _createRenderTarget(params)
|
||||
{
|
||||
|
||||
const cubeUVRenderTarget = new WebGLRenderTarget(3 * SIZE_MAX, 3 * SIZE_MAX, params);
|
||||
cubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping;
|
||||
cubeUVRenderTarget.texture.name = 'PMREM.cubeUv';
|
||||
cubeUVRenderTarget.scissorTest = true;
|
||||
return cubeUVRenderTarget;
|
||||
|
||||
}
|
||||
|
||||
function _setViewport(target, x, y, width, height)
|
||||
{
|
||||
|
||||
target.viewport.set(x, y, width, height);
|
||||
target.scissor.set(x, y, width, height);
|
||||
|
||||
}
|
||||
|
||||
function _getBlurShader(maxSamples)
|
||||
{
|
||||
|
||||
const weights = new Float32Array(maxSamples);
|
||||
const poleAxis = new Vector3(0, 1, 0);
|
||||
const shaderMaterial = new RawShaderMaterial({
|
||||
|
||||
name: 'SphericalGaussianBlur',
|
||||
|
||||
defines: { 'n': maxSamples },
|
||||
|
||||
uniforms: {
|
||||
'envMap': { value: null },
|
||||
'samples': { value: 1 },
|
||||
'weights': { value: weights },
|
||||
'latitudinal': { value: false },
|
||||
'dTheta': { value: 0 },
|
||||
'mipInt': { value: 0 },
|
||||
'poleAxis': { value: poleAxis },
|
||||
'inputEncoding': { value: ENCODINGS[LinearEncoding] },
|
||||
'outputEncoding': { value: ENCODINGS[LinearEncoding] }
|
||||
},
|
||||
|
||||
vertexShader: _getCommonVertexShader(),
|
||||
|
||||
fragmentShader: /* glsl */`
|
||||
|
||||
precision mediump float;
|
||||
precision mediump int;
|
||||
|
||||
varying vec3 vOutputDirection;
|
||||
|
||||
uniform sampler2D envMap;
|
||||
uniform int samples;
|
||||
uniform float weights[ n ];
|
||||
uniform bool latitudinal;
|
||||
uniform float dTheta;
|
||||
uniform float mipInt;
|
||||
uniform vec3 poleAxis;
|
||||
|
||||
${_getEncodings()}
|
||||
|
||||
#define ENVMAP_TYPE_CUBE_UV
|
||||
#include <cube_uv_reflection_fragment>
|
||||
|
||||
vec3 getSample( float theta, vec3 axis ) {
|
||||
|
||||
float cosTheta = cos( theta );
|
||||
// Rodrigues' axis-angle rotation
|
||||
vec3 sampleDirection = vOutputDirection * cosTheta
|
||||
+ cross( axis, vOutputDirection ) * sin( theta )
|
||||
+ axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta );
|
||||
|
||||
return bilinearCubeUV( envMap, sampleDirection, mipInt );
|
||||
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
||||
vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection );
|
||||
|
||||
if ( all( equal( axis, vec3( 0.0 ) ) ) ) {
|
||||
|
||||
axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x );
|
||||
|
||||
}
|
||||
|
||||
axis = normalize( axis );
|
||||
|
||||
gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );
|
||||
gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis );
|
||||
|
||||
for ( int i = 1; i < n; i++ ) {
|
||||
|
||||
if ( i >= samples ) {
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
float theta = dTheta * float( i );
|
||||
gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis );
|
||||
gl_FragColor.rgb += weights[ i ] * getSample( theta, axis );
|
||||
|
||||
}
|
||||
|
||||
gl_FragColor = linearToOutputTexel( gl_FragColor );
|
||||
|
||||
}
|
||||
`,
|
||||
|
||||
blending: NoBlending,
|
||||
depthTest: false,
|
||||
depthWrite: false
|
||||
|
||||
});
|
||||
|
||||
return shaderMaterial;
|
||||
|
||||
}
|
||||
|
||||
function _getEquirectShader()
|
||||
{
|
||||
|
||||
const texelSize = new Vector2(1, 1);
|
||||
const shaderMaterial = new RawShaderMaterial({
|
||||
|
||||
name: 'EquirectangularToCubeUV',
|
||||
|
||||
uniforms: {
|
||||
'envMap': { value: null },
|
||||
'texelSize': { value: texelSize },
|
||||
'inputEncoding': { value: ENCODINGS[LinearEncoding] },
|
||||
'outputEncoding': { value: ENCODINGS[LinearEncoding] }
|
||||
},
|
||||
|
||||
vertexShader: _getCommonVertexShader(),
|
||||
|
||||
fragmentShader: /* glsl */`
|
||||
|
||||
precision mediump float;
|
||||
precision mediump int;
|
||||
|
||||
varying vec3 vOutputDirection;
|
||||
|
||||
uniform sampler2D envMap;
|
||||
uniform vec2 texelSize;
|
||||
|
||||
${_getEncodings()}
|
||||
|
||||
#include <common>
|
||||
|
||||
void main() {
|
||||
|
||||
gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );
|
||||
|
||||
vec3 outputDirection = normalize( vOutputDirection );
|
||||
vec2 uv = equirectUv( outputDirection );
|
||||
|
||||
vec2 f = fract( uv / texelSize - 0.5 );
|
||||
uv -= f * texelSize;
|
||||
vec3 tl = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;
|
||||
uv.x += texelSize.x;
|
||||
vec3 tr = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;
|
||||
uv.y += texelSize.y;
|
||||
vec3 br = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;
|
||||
uv.x -= texelSize.x;
|
||||
vec3 bl = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;
|
||||
|
||||
vec3 tm = mix( tl, tr, f.x );
|
||||
vec3 bm = mix( bl, br, f.x );
|
||||
gl_FragColor.rgb = mix( tm, bm, f.y );
|
||||
|
||||
gl_FragColor = linearToOutputTexel( gl_FragColor );
|
||||
|
||||
}
|
||||
`,
|
||||
|
||||
blending: NoBlending,
|
||||
depthTest: false,
|
||||
depthWrite: false
|
||||
|
||||
});
|
||||
|
||||
return shaderMaterial;
|
||||
|
||||
}
|
||||
|
||||
function _getCubemapShader()
|
||||
{
|
||||
|
||||
const shaderMaterial = new RawShaderMaterial({
|
||||
|
||||
name: 'CubemapToCubeUV',
|
||||
|
||||
uniforms: {
|
||||
'envMap': { value: null },
|
||||
'inputEncoding': { value: ENCODINGS[LinearEncoding] },
|
||||
'outputEncoding': { value: ENCODINGS[LinearEncoding] }
|
||||
},
|
||||
|
||||
vertexShader: _getCommonVertexShader(),
|
||||
|
||||
fragmentShader: /* glsl */`
|
||||
|
||||
precision mediump float;
|
||||
precision mediump int;
|
||||
|
||||
varying vec3 vOutputDirection;
|
||||
|
||||
uniform samplerCube envMap;
|
||||
|
||||
${_getEncodings()}
|
||||
|
||||
void main() {
|
||||
|
||||
gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );
|
||||
gl_FragColor.rgb = envMapTexelToLinear( textureCube( envMap, vec3( - vOutputDirection.x, vOutputDirection.yz ) ) ).rgb;
|
||||
gl_FragColor = linearToOutputTexel( gl_FragColor );
|
||||
|
||||
}
|
||||
`,
|
||||
|
||||
blending: NoBlending,
|
||||
depthTest: false,
|
||||
depthWrite: false
|
||||
|
||||
});
|
||||
|
||||
return shaderMaterial;
|
||||
|
||||
}
|
||||
|
||||
function _getCommonVertexShader()
|
||||
{
|
||||
|
||||
return /* glsl */`
|
||||
|
||||
precision mediump float;
|
||||
precision mediump int;
|
||||
|
||||
attribute vec3 position;
|
||||
attribute vec2 uv;
|
||||
attribute float faceIndex;
|
||||
|
||||
varying vec3 vOutputDirection;
|
||||
|
||||
// RH coordinate system; PMREM face-indexing convention
|
||||
vec3 getDirection( vec2 uv, float face ) {
|
||||
|
||||
uv = 2.0 * uv - 1.0;
|
||||
|
||||
vec3 direction = vec3( uv, 1.0 );
|
||||
|
||||
if ( face == 0.0 ) {
|
||||
|
||||
direction = direction.zyx; // ( 1, v, u ) pos x
|
||||
|
||||
} else if ( face == 1.0 ) {
|
||||
|
||||
direction = direction.xzy;
|
||||
direction.xz *= -1.0; // ( -u, 1, -v ) pos y
|
||||
|
||||
} else if ( face == 2.0 ) {
|
||||
|
||||
direction.x *= -1.0; // ( -u, v, 1 ) pos z
|
||||
|
||||
} else if ( face == 3.0 ) {
|
||||
|
||||
direction = direction.zyx;
|
||||
direction.xz *= -1.0; // ( -1, v, -u ) neg x
|
||||
|
||||
} else if ( face == 4.0 ) {
|
||||
|
||||
direction = direction.xzy;
|
||||
direction.xy *= -1.0; // ( -u, -1, v ) neg y
|
||||
|
||||
} else if ( face == 5.0 ) {
|
||||
|
||||
direction.z *= -1.0; // ( u, v, -1 ) neg z
|
||||
|
||||
}
|
||||
|
||||
return direction;
|
||||
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
||||
vOutputDirection = getDirection( uv, faceIndex );
|
||||
|
||||
// //从xz->z-up坐标系变换到 threejs坐标系
|
||||
mat3 ro = mat3(
|
||||
1.0, 0.0, 0.0,
|
||||
0.0, 0.0, -1.0,
|
||||
0.0, 1.0, 0
|
||||
);
|
||||
vOutputDirection = ro * vOutputDirection;
|
||||
|
||||
gl_Position = vec4( position, 1.0 );
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
function _getEncodings()
|
||||
{
|
||||
|
||||
return /* glsl */`
|
||||
|
||||
uniform int inputEncoding;
|
||||
uniform int outputEncoding;
|
||||
|
||||
#include <encodings_pars_fragment>
|
||||
|
||||
vec4 inputTexelToLinear( vec4 value ) {
|
||||
|
||||
if ( inputEncoding == 0 ) {
|
||||
|
||||
return value;
|
||||
|
||||
} else if ( inputEncoding == 1 ) {
|
||||
|
||||
return sRGBToLinear( value );
|
||||
|
||||
} else if ( inputEncoding == 2 ) {
|
||||
|
||||
return RGBEToLinear( value );
|
||||
|
||||
} else if ( inputEncoding == 3 ) {
|
||||
|
||||
return RGBMToLinear( value, 7.0 );
|
||||
|
||||
} else if ( inputEncoding == 4 ) {
|
||||
|
||||
return RGBMToLinear( value, 16.0 );
|
||||
|
||||
} else if ( inputEncoding == 5 ) {
|
||||
|
||||
return RGBDToLinear( value, 256.0 );
|
||||
|
||||
} else {
|
||||
|
||||
return GammaToLinear( value, 2.2 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
vec4 linearToOutputTexel( vec4 value ) {
|
||||
|
||||
if ( outputEncoding == 0 ) {
|
||||
|
||||
return value;
|
||||
|
||||
} else if ( outputEncoding == 1 ) {
|
||||
|
||||
return LinearTosRGB( value );
|
||||
|
||||
} else if ( outputEncoding == 2 ) {
|
||||
|
||||
return LinearToRGBE( value );
|
||||
|
||||
} else if ( outputEncoding == 3 ) {
|
||||
|
||||
return LinearToRGBM( value, 7.0 );
|
||||
|
||||
} else if ( outputEncoding == 4 ) {
|
||||
|
||||
return LinearToRGBM( value, 16.0 );
|
||||
|
||||
} else if ( outputEncoding == 5 ) {
|
||||
|
||||
return LinearToRGBD( value, 256.0 );
|
||||
|
||||
} else {
|
||||
|
||||
return LinearToGamma( value, 2.2 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
vec4 envMapTexelToLinear( vec4 color ) {
|
||||
|
||||
return inputTexelToLinear( color );
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
}
|
46
src/common/PlaneExt.ts
Normal file
46
src/common/PlaneExt.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as THREE from 'three';
|
||||
import { Vector3, Line3, Plane } from "three";
|
||||
|
||||
export class PlaneExt extends Plane
|
||||
{
|
||||
constructor(normal?: Vector3, constant?: number)
|
||||
{
|
||||
super(normal, constant);
|
||||
}
|
||||
intersectLine(line: Line3, optionalTarget?: Vector3, extendLine?: boolean): Vector3
|
||||
{
|
||||
let v1 = new Vector3();
|
||||
|
||||
let result = optionalTarget || new Vector3();
|
||||
|
||||
let direction = line.delta(v1);
|
||||
|
||||
let denominator = this.normal.dot(direction);
|
||||
|
||||
if (denominator === 0)
|
||||
{
|
||||
// line is coplanar, return origin
|
||||
if (this.distanceToPoint(line.start) === 0)
|
||||
{
|
||||
return result.copy(line.start);
|
||||
}
|
||||
// Unsure if this is the correct method to handle this case.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let t = - (line.start.dot(this.normal) + this.constant) / denominator;
|
||||
//If you not extendLine,check intersect point in Line
|
||||
if (!extendLine && (t < 0 || t > 1))
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result.copy(direction).multiplyScalar(t).add(line.start);
|
||||
}
|
||||
intersectRay(ray: THREE.Ray, optionalTarget?: Vector3, extendLine?: boolean): Vector3
|
||||
{
|
||||
// 从射线初始位置
|
||||
let line = new THREE.Line3(ray.origin.clone(), ray.origin.clone().add(ray.direction));
|
||||
return this.intersectLine(line, optionalTarget, extendLine);
|
||||
}
|
||||
}
|
40
src/common/Singleton.ts
Normal file
40
src/common/Singleton.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
|
||||
let instanceMap = new Map();
|
||||
|
||||
export interface PrototypeType<T> extends Function
|
||||
{
|
||||
prototype: T;
|
||||
}
|
||||
|
||||
export interface ConstructorFunctionType<T = any> extends PrototypeType<T>
|
||||
{
|
||||
new(...args: any[]): T;
|
||||
}
|
||||
|
||||
export type ConstructorType<T = unknown, Static extends Record<string, any> = PrototypeType<T>> = (ConstructorFunctionType<T> | PrototypeType<T>) & {
|
||||
[Key in keyof Static]: Static[Key];
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造单例类的静态类.
|
||||
* # Example:
|
||||
* class A extends Singleton(){};
|
||||
* //获得单例
|
||||
* let a = A.GetInstance();
|
||||
*/
|
||||
export class Singleton
|
||||
{
|
||||
protected constructor() { }
|
||||
|
||||
//ref:https://github.com/Microsoft/TypeScript/issues/5863
|
||||
static GetInstance<T extends Singleton>(this: ConstructorType<T, typeof Singleton>): T
|
||||
{
|
||||
if (instanceMap.has(this))
|
||||
return instanceMap.get(this);
|
||||
//@ts-ignore
|
||||
let __instance__ = new this.prototype.constructor();
|
||||
instanceMap.set(this, __instance__);
|
||||
return __instance__;
|
||||
}
|
||||
}
|
168
src/common/Viewer.ts
Normal file
168
src/common/Viewer.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Raycaster, Scene, Vector3, WebGLRenderer, type WebGLRendererParameters } from "three";
|
||||
import { cZeroVec, GetBox, GetBoxArr } from "./GeUtils";
|
||||
import { PlaneExt } from "./PlaneExt";
|
||||
import { CameraUpdate } from "webcad_ue4_api";
|
||||
import { CameraControls } from "./CameraControls";
|
||||
|
||||
export class Viewer {
|
||||
m_LookTarget: any;
|
||||
m_Camera: CameraUpdate = new CameraUpdate();
|
||||
CameraCtrl: CameraControls;
|
||||
protected NeedUpdate: boolean = true;
|
||||
Renderer: WebGLRenderer;//渲染器 //暂时只用这个类型
|
||||
m_DomEl: HTMLElement; //画布容器
|
||||
|
||||
_Height: number = 0;
|
||||
_Width: number = 0;
|
||||
|
||||
_Scene: Scene = new Scene();
|
||||
|
||||
get Scene() {
|
||||
return this._Scene;
|
||||
}
|
||||
|
||||
UpdateRender()
|
||||
{
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} canvasContainer 可以传入一个div或者一个画布
|
||||
* @memberof Viewer
|
||||
*/
|
||||
constructor(canvasContainer: HTMLElement) {
|
||||
|
||||
this.m_DomEl = canvasContainer;
|
||||
this.initRender(canvasContainer);
|
||||
this.OnSize();
|
||||
this.StartRender();
|
||||
this.CameraCtrl = new CameraControls(this);
|
||||
window.addEventListener("resize", () => {
|
||||
this.OnSize();
|
||||
});
|
||||
}
|
||||
|
||||
//初始化render
|
||||
initRender(canvasContainer: HTMLElement) {
|
||||
let params: WebGLRendererParameters = {
|
||||
antialias: true,//antialias:true/false是否开启反锯齿
|
||||
precision: "highp",//precision:highp/mediump/lowp着色精度选择
|
||||
alpha: true//alpha:true/false是否可以设置背景色透明
|
||||
};
|
||||
this.Renderer = new WebGLRenderer(params);
|
||||
//加到画布
|
||||
canvasContainer.appendChild(this.Renderer.domElement);
|
||||
|
||||
this.Renderer.autoClear = true;
|
||||
|
||||
//如果设置,那么它希望所有的纹理和颜色都是预乘的伽玛。默认值为false。
|
||||
// this.Renderer.gammaInput = true;
|
||||
// this.Renderer.gammaOutput = true;
|
||||
// this.Renderer.shadowMap.enabled = true;
|
||||
// this.Renderer.toneMapping = ReinhardToneMapping;
|
||||
//设置设备像素比。 这通常用于HiDPI设备,以防止模糊输出画布。
|
||||
this.Renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.Renderer.physicallyCorrectLights = true;
|
||||
//this.Renderer.toneMappingExposure = Math.pow(1, 5.0); // to allow for very bright scenes.
|
||||
|
||||
//设置它的背景色为黑色
|
||||
this.Renderer.setClearColor(0xffffff, 1);
|
||||
|
||||
|
||||
this.OnSize();
|
||||
}
|
||||
|
||||
OnSize = (width?: number, height?: number) => {
|
||||
this._Width = width ? width : this.m_DomEl.clientWidth;
|
||||
this._Height = height ? height : this.m_DomEl.clientHeight;
|
||||
|
||||
//校验.成为2的倍数 避免外轮廓错误.
|
||||
if (this._Width % 2 == 1)
|
||||
this._Width -= 1;
|
||||
if (this._Height % 2 == 1)
|
||||
this._Height -= 1;
|
||||
|
||||
this.Renderer.setSize(this._Width, this._Height);
|
||||
this.m_Camera.SetSize(this._Width, this._Height);
|
||||
};
|
||||
|
||||
StartRender = () => {
|
||||
requestAnimationFrame(this.StartRender);
|
||||
if (this._Scene != null && this.NeedUpdate) {
|
||||
this.Render();
|
||||
this.NeedUpdate = false;
|
||||
}
|
||||
};
|
||||
Render() {
|
||||
this.Renderer.render(this._Scene, this.m_Camera.Camera);
|
||||
}
|
||||
|
||||
ScreenToWorld(pt: Vector3, planVec?: Vector3) {
|
||||
//变换和求交点
|
||||
let plan = new PlaneExt(planVec || new Vector3(0, 0, 1));
|
||||
let raycaster = new Raycaster();
|
||||
// 射线从相机射线向屏幕点位置
|
||||
raycaster.setFromCamera(
|
||||
{
|
||||
x: (pt.x / this._Width) * 2 - 1,
|
||||
y: - (pt.y / this._Height) * 2 + 1
|
||||
}
|
||||
, this.m_Camera.Camera
|
||||
);
|
||||
plan.intersectRay(raycaster.ray, pt, true);
|
||||
}
|
||||
WorldToScreen(pt: Vector3) {
|
||||
let widthHalf = this._Width * 0.5;
|
||||
let heightHalf = this._Height * 0.5;
|
||||
|
||||
pt.project(this.m_Camera.Camera);
|
||||
|
||||
pt.x = (pt.x * widthHalf) + widthHalf;
|
||||
pt.y = - (pt.y * heightHalf) + heightHalf;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新视角观测目标(物体中心)
|
||||
*
|
||||
* @memberof Viewer
|
||||
*/
|
||||
UpdateLockTarget() {
|
||||
let renderList = this.Renderer.renderLists.get(this._Scene, this.m_Camera.Camera);
|
||||
let box = GetBoxArr(renderList.opaque.map(o => o.object));
|
||||
if (box)
|
||||
this.m_LookTarget = box.getCenter(new Vector3());
|
||||
else
|
||||
this.m_LookTarget = cZeroVec;
|
||||
}
|
||||
Rotate(mouseMove: Vector3) {
|
||||
this.m_Camera.Rotate(mouseMove, this.m_LookTarget);
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
Pan(mouseMove: Vector3) {
|
||||
this.m_Camera.Pan(mouseMove);
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
Zoom(scale: number, center?: Vector3) {
|
||||
this.m_Camera.Zoom(scale, center);
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
ZoomAll()
|
||||
{
|
||||
this.m_Camera.ZoomExtentsBox3(GetBox(this._Scene, true));
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
|
||||
ViewToTop() {
|
||||
this.m_Camera.LookAt(new Vector3(0, 0, -1));
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
ViewToFront() {
|
||||
this.m_Camera.LookAt(new Vector3(0, 1, 0));
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
ViewToSwiso() {
|
||||
this.m_Camera.LookAt(new Vector3(1, 1, -1));
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
}
|
41
src/components/MaterialView.vue
Normal file
41
src/components/MaterialView.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div ref="container" style="width: 800px;height: 800px;"></div>
|
||||
{{ CurGeometryName }}
|
||||
<div v-for="geo in geometries">
|
||||
<button @click="changeGeometry(geo[0])">{{ geo[0] }}</button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import { MaterialEditor } from '../common/MaterialEditor';
|
||||
import { PhysicalMaterialRecord } from 'webcad_ue4_api';
|
||||
|
||||
const container = useTemplateRef<HTMLElement>('container');
|
||||
|
||||
|
||||
let editor:MaterialEditor = MaterialEditor.GetInstance();
|
||||
const geometries = editor.Geometrys;
|
||||
const material = new PhysicalMaterialRecord();
|
||||
|
||||
const CurGeometryName = editor.CurGeometryName;
|
||||
onMounted(() => {
|
||||
editor.SetViewer(container.value);
|
||||
editor.setMaterial(material);
|
||||
|
||||
const view = editor.Viewer;
|
||||
view.OnSize(800, 800);
|
||||
view.ZoomAll();
|
||||
view.Zoom(2.1);
|
||||
})
|
||||
|
||||
function changeGeometry(geoName:string) {
|
||||
CurGeometryName.value = geoName;
|
||||
let geo = editor.Geometrys.get(CurGeometryName.value);
|
||||
if (geo) {
|
||||
editor.ShowMesh.geometry = geo;
|
||||
editor.Viewer.UpdateRender();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
6
src/main.ts
Normal file
6
src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
Reference in New Issue
Block a user