|
@@ -12,14 +12,19 @@ import {
|
|
|
} from "../../models";
|
|
|
import {
|
|
|
Clock,
|
|
|
+ Color,
|
|
|
GammaEncoding,
|
|
|
LinearFilter,
|
|
|
Mesh,
|
|
|
MeshStandardMaterial,
|
|
|
PointLight,
|
|
|
+ Texture,
|
|
|
Vector2,
|
|
|
Vector3,
|
|
|
} from "three-platformize";
|
|
|
+import { OutlinePass } from "three-platformize/examples/jsm/postprocessing/OutlinePass";
|
|
|
+import { RenderPass } from "three-platformize/examples/jsm/postprocessing/RenderPass";
|
|
|
+import { EffectComposer } from "three-platformize/examples/jsm/postprocessing/EffectComposer";
|
|
|
import { Easing, Tween } from "@tweenjs/tween.js";
|
|
|
import { cancelAnimationFrame, requestAnimationFrame } from "@tarojs/runtime";
|
|
|
import { CanvasAdapter } from "../../components";
|
|
@@ -29,6 +34,8 @@ enum MODEL_STATE {
|
|
|
DEFAULT = 0,
|
|
|
ZOOM_UP = 1,
|
|
|
ZOOM_UP_CLICK = 2,
|
|
|
+ CHECK_WEN = 3,
|
|
|
+ CHECK_HUO = 4,
|
|
|
}
|
|
|
|
|
|
interface TweenHandlerOptions<T extends object> {
|
|
@@ -49,8 +56,11 @@ const DENGZHAO_MIN_ROTATE = -1.2;
|
|
|
const DENGZHAO_MAX_ROTATE = -2.55;
|
|
|
const system = Taro.getSystemInfoSync();
|
|
|
|
|
|
+const meshMap = new Map<string, Texture>();
|
|
|
+
|
|
|
const IndexPage: FC = observer(() => {
|
|
|
const clock = useRef(new Clock());
|
|
|
+ const effectCompose = useRef<EffectComposer>();
|
|
|
const bulbLight = useRef<PointLight>();
|
|
|
const cameraPosition = useRef(new Vector3(0, 100, 360));
|
|
|
/**
|
|
@@ -60,6 +70,7 @@ const IndexPage: FC = observer(() => {
|
|
|
const [modelState, setModelState] = useState<MODEL_STATE>(
|
|
|
MODEL_STATE.DEFAULT
|
|
|
);
|
|
|
+ const _modelState = useRef<MODEL_STATE>(modelState);
|
|
|
/**
|
|
|
* 是否分解
|
|
|
*/
|
|
@@ -74,10 +85,21 @@ const IndexPage: FC = observer(() => {
|
|
|
() => modelState === MODEL_STATE.ZOOM_UP_CLICK,
|
|
|
[modelState]
|
|
|
);
|
|
|
+ /**
|
|
|
+ * 是否展示花纹
|
|
|
+ */
|
|
|
+ const isWen = useMemo(
|
|
|
+ () => modelState === MODEL_STATE.CHECK_WEN,
|
|
|
+ [modelState]
|
|
|
+ );
|
|
|
const hotSeparateAnimArr = useRef<ReticleModel[]>([]);
|
|
|
+ const hotspotAnimArr = useRef<ReticleModel[]>([]);
|
|
|
const tagArr = useRef<TagModel[]>([]);
|
|
|
const startMouse = useRef(new Vector2(0, 0));
|
|
|
const mouseV2 = useRef(new Vector2(0, 0));
|
|
|
+ const wenCurStep = useRef(0);
|
|
|
+ const wenStep = useRef(0.01);
|
|
|
+ const outlinePass = useRef<OutlinePass>();
|
|
|
|
|
|
useEffect(() => {
|
|
|
setTimeout(async () => {
|
|
@@ -89,8 +111,13 @@ const IndexPage: FC = observer(() => {
|
|
|
return renderModel.dispose;
|
|
|
}, []);
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
+ _modelState.current = modelState;
|
|
|
+ }, [modelState]);
|
|
|
+
|
|
|
const init = async () => {
|
|
|
await renderModel.init("#wgl");
|
|
|
+ initOutlinePass();
|
|
|
|
|
|
// 创建点光源
|
|
|
bulbLight.current = new PointLight(16772744, 2.5, 50, 2);
|
|
@@ -153,6 +180,8 @@ const IndexPage: FC = observer(() => {
|
|
|
mesh.material.dispose();
|
|
|
mesh.material = meshStandardMaterial;
|
|
|
mesh.material.needsUpdate = true;
|
|
|
+
|
|
|
+ meshMap.set(mesh.name, meshStandardMaterial.map!);
|
|
|
});
|
|
|
|
|
|
setTimeout(() => {
|
|
@@ -170,6 +199,7 @@ const IndexPage: FC = observer(() => {
|
|
|
renderModel.model?.scale.copy(new Vector3(e.x, e.x, e.x));
|
|
|
},
|
|
|
cb: () => {
|
|
|
+ handleAnimatorCallout(true);
|
|
|
setTimeout(() => {
|
|
|
renderModel.setAutoRotate(ROTATE_TYPE.TRUE);
|
|
|
}, 800);
|
|
@@ -327,53 +357,6 @@ const IndexPage: FC = observer(() => {
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
- * 恢复模型
|
|
|
- */
|
|
|
- const revertModel = (cb?: Function) => {
|
|
|
- const pos = renderModel.camera.position.clone();
|
|
|
- const lookAt = renderModel.controls.target.clone();
|
|
|
- const targetPos = new Vector3(0, 30, 372);
|
|
|
- const targetLookAt = new Vector3(0, 0, 0);
|
|
|
-
|
|
|
- tweenHandler({
|
|
|
- curProps: {
|
|
|
- x1: pos.x,
|
|
|
- y1: pos.y,
|
|
|
- z1: pos.z,
|
|
|
- x2: lookAt.x,
|
|
|
- y2: lookAt.y,
|
|
|
- z2: lookAt.z,
|
|
|
- },
|
|
|
- targetProps: {
|
|
|
- x1: targetPos.x,
|
|
|
- y1: targetPos.y,
|
|
|
- z1: targetPos.z,
|
|
|
- x2: targetLookAt.x,
|
|
|
- y2: targetLookAt.y,
|
|
|
- z2: targetLookAt.z,
|
|
|
- },
|
|
|
- onUpdate: (e) => {
|
|
|
- renderModel.camera.position.set(e.x1, e.y1, e.z1);
|
|
|
- renderModel.controls.target.set(e.x2, e.y2, e.z2);
|
|
|
- renderModel.controls.update();
|
|
|
- },
|
|
|
- cb,
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- const handleControlsDistance = (dis: number, maxDis = 470) => {
|
|
|
- renderModel.controls.minDistance = dis;
|
|
|
- renderModel.controls.maxDistance = maxDis;
|
|
|
- };
|
|
|
-
|
|
|
- /**
|
|
|
- * 修改控制器垂直最大角度
|
|
|
- */
|
|
|
- const handleControlsAngle = (type: boolean) => {
|
|
|
- renderModel.controls.maxPolarAngle = type ? 0.59 * Math.PI : Math.PI;
|
|
|
- };
|
|
|
-
|
|
|
- /**
|
|
|
* 模型分离动画
|
|
|
* @param type 是否分离
|
|
|
*/
|
|
@@ -407,15 +390,17 @@ const IndexPage: FC = observer(() => {
|
|
|
loadCount++;
|
|
|
|
|
|
if (loadCount === 5) {
|
|
|
- initTag(true);
|
|
|
- initReticleMeshs(true);
|
|
|
+ handleAnimatorCallout(false);
|
|
|
+ handleTag(true);
|
|
|
+ handleReticleMeshs(true);
|
|
|
cb && cb();
|
|
|
}
|
|
|
},
|
|
|
});
|
|
|
} else {
|
|
|
- initTag(false);
|
|
|
- initReticleMeshs(false);
|
|
|
+ handleTag(false);
|
|
|
+ handleReticleMeshs(false);
|
|
|
+ handleAnimatorCallout(true);
|
|
|
tweenHandler({
|
|
|
curProps: item.position,
|
|
|
targetProps: item._position,
|
|
@@ -425,7 +410,54 @@ const IndexPage: FC = observer(() => {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
- const initTag = (visible: boolean) => {
|
|
|
+ /**
|
|
|
+ * 恢复模型
|
|
|
+ */
|
|
|
+ const revertModel = (cb?: Function) => {
|
|
|
+ const pos = renderModel.camera.position.clone();
|
|
|
+ const lookAt = renderModel.controls.target.clone();
|
|
|
+ const targetPos = new Vector3(0, 30, 372);
|
|
|
+ const targetLookAt = new Vector3(0, 0, 0);
|
|
|
+
|
|
|
+ tweenHandler({
|
|
|
+ curProps: {
|
|
|
+ x1: pos.x,
|
|
|
+ y1: pos.y,
|
|
|
+ z1: pos.z,
|
|
|
+ x2: lookAt.x,
|
|
|
+ y2: lookAt.y,
|
|
|
+ z2: lookAt.z,
|
|
|
+ },
|
|
|
+ targetProps: {
|
|
|
+ x1: targetPos.x,
|
|
|
+ y1: targetPos.y,
|
|
|
+ z1: targetPos.z,
|
|
|
+ x2: targetLookAt.x,
|
|
|
+ y2: targetLookAt.y,
|
|
|
+ z2: targetLookAt.z,
|
|
|
+ },
|
|
|
+ onUpdate: (e) => {
|
|
|
+ renderModel.camera.position.set(e.x1, e.y1, e.z1);
|
|
|
+ renderModel.controls.target.set(e.x2, e.y2, e.z2);
|
|
|
+ renderModel.controls.update();
|
|
|
+ },
|
|
|
+ cb,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleControlsDistance = (dis: number, maxDis = 470) => {
|
|
|
+ renderModel.controls.minDistance = dis;
|
|
|
+ renderModel.controls.maxDistance = maxDis;
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 修改控制器垂直最大角度
|
|
|
+ */
|
|
|
+ const handleControlsAngle = (type: boolean) => {
|
|
|
+ renderModel.controls.maxPolarAngle = type ? 0.59 * Math.PI : Math.PI;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleTag = (visible: boolean) => {
|
|
|
if (!tagArr.current.length) {
|
|
|
const temp: TagModel[] = [];
|
|
|
const tagStack: TagModelParams[] = [
|
|
@@ -464,7 +496,7 @@ const IndexPage: FC = observer(() => {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
- const initReticleMeshs = (visible: boolean) => {
|
|
|
+ const handleReticleMeshs = (visible: boolean) => {
|
|
|
if (!hotSeparateAnimArr.current.length) {
|
|
|
const temp: ReticleModel[] = [];
|
|
|
const vectorStack = [
|
|
@@ -497,6 +529,40 @@ const IndexPage: FC = observer(() => {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+ const handleAnimatorCallout = (visible: boolean) => {
|
|
|
+ if (!hotspotAnimArr.current.length) {
|
|
|
+ const hot = new ReticleModel({
|
|
|
+ name: "huo",
|
|
|
+ url: require("./resource/hot_anim.png"),
|
|
|
+ planeWidth: 250,
|
|
|
+ tilesHorizontal: 7,
|
|
|
+ tilesVertical: 1,
|
|
|
+ tileDisplayDuration: 200,
|
|
|
+ scale: 0.0022,
|
|
|
+ position: new Vector3(0, 0.2, 0.65),
|
|
|
+ camera: renderModel.camera,
|
|
|
+ });
|
|
|
+ const wen = new ReticleModel({
|
|
|
+ name: "wen",
|
|
|
+ url: require("./resource/wen_anim.png"),
|
|
|
+ planeWidth: 250,
|
|
|
+ tilesHorizontal: 7,
|
|
|
+ tilesVertical: 1,
|
|
|
+ tileDisplayDuration: 200,
|
|
|
+ scale: 0.0022,
|
|
|
+ position: new Vector3(0, -1, 0.65),
|
|
|
+ camera: renderModel.camera,
|
|
|
+ });
|
|
|
+
|
|
|
+ renderModel.model?.add(hot.reticleAnim, wen.reticleAnim);
|
|
|
+ hotspotAnimArr.current.push(hot, wen);
|
|
|
+ }
|
|
|
+
|
|
|
+ hotspotAnimArr.current.forEach((item) => {
|
|
|
+ visible ? item.show() : item.hide();
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
const animate = () => {
|
|
|
const time = clock.current.getDelta();
|
|
|
|
|
@@ -504,6 +570,28 @@ const IndexPage: FC = observer(() => {
|
|
|
item.update(time);
|
|
|
});
|
|
|
|
|
|
+ hotspotAnimArr.current.forEach((item) => {
|
|
|
+ item.update(time);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (_modelState.current === MODEL_STATE.CHECK_WEN) {
|
|
|
+ wenCurStep.current += wenStep.current;
|
|
|
+
|
|
|
+ if (wenCurStep.current > 1) {
|
|
|
+ wenCurStep.current = 1;
|
|
|
+ wenStep.current = -0.01;
|
|
|
+ } else if (wenCurStep.current <= 0.1) {
|
|
|
+ wenCurStep.current = 0.1;
|
|
|
+ wenStep.current = 0.01;
|
|
|
+ }
|
|
|
+
|
|
|
+ renderModel.modelMaterialList.forEach((item) => {
|
|
|
+ item.material.opacity = wenCurStep.current;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ effectCompose.current && effectCompose.current.render();
|
|
|
+
|
|
|
requestAnimationFrame(animate);
|
|
|
};
|
|
|
|
|
@@ -518,22 +606,72 @@ const IndexPage: FC = observer(() => {
|
|
|
for (let i = 0; i < res.length; i++) {
|
|
|
const name = res[i].object.name;
|
|
|
|
|
|
- if (!name) continue;
|
|
|
+ if (!name || !res[i].object.visible || !res[i].object.parent?.visible)
|
|
|
+ continue;
|
|
|
|
|
|
console.log("click model:", name);
|
|
|
|
|
|
- if (
|
|
|
- isSeparate &&
|
|
|
- res[i].object.visible &&
|
|
|
- res[i].object.parent?.visible
|
|
|
- ) {
|
|
|
+ if (isSeparate) {
|
|
|
handleModelZoomUp(name);
|
|
|
break;
|
|
|
+ } else if (modelState === MODEL_STATE.DEFAULT) {
|
|
|
+ if (name === "wen") {
|
|
|
+ wenliHandler(true);
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const wenliHandler = (visible: boolean) => {
|
|
|
+ revertModel(() => {
|
|
|
+ if (visible) {
|
|
|
+ handleAnimatorCallout(false);
|
|
|
+ handleWenLi(true);
|
|
|
+ setModelState(MODEL_STATE.CHECK_WEN);
|
|
|
+ } else {
|
|
|
+ handleAnimatorCallout(true);
|
|
|
+ handleWenLi(false);
|
|
|
+ setModelState(MODEL_STATE.DEFAULT);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleWenLi = (visible: boolean) => {
|
|
|
+ const models = [
|
|
|
+ "tongniudengdizuo",
|
|
|
+ "dengzhao2",
|
|
|
+ "dengzhao1",
|
|
|
+ "dengpan",
|
|
|
+ "dengguan",
|
|
|
+ ];
|
|
|
+ renderModel.modelMaterialList.forEach((model) => {
|
|
|
+ if (!models.includes(model.name)) return;
|
|
|
+
|
|
|
+ if (visible) {
|
|
|
+ model.material.map = model.material.emissiveMap;
|
|
|
+ } else {
|
|
|
+ model.material.map = meshMap.get(model.name);
|
|
|
+ model.material.map.magFilter = LinearFilter;
|
|
|
+ model.material.map.minFilter = LinearFilter;
|
|
|
+ model.material.map.encoding = GammaEncoding;
|
|
|
+ model.material.opacity = 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ model.material.dispose();
|
|
|
+ model.material.needsUpdate = true;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (visible) {
|
|
|
+ outlinePass.current?.selectedObjects.push(
|
|
|
+ ...renderModel.modelMaterialList
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ outlinePass.current!.selectedObjects = [];
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
/**
|
|
|
* 单独展示某个模型
|
|
|
*/
|
|
@@ -570,8 +708,8 @@ const IndexPage: FC = observer(() => {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- initTag(false);
|
|
|
- initReticleMeshs(false);
|
|
|
+ handleTag(false);
|
|
|
+ handleReticleMeshs(false);
|
|
|
tweenHandler({
|
|
|
curProps: {
|
|
|
x: renderModel.model!.position.x,
|
|
@@ -589,8 +727,8 @@ const IndexPage: FC = observer(() => {
|
|
|
renderModel.modelMaterialList.forEach((model) => {
|
|
|
model.visible = true;
|
|
|
});
|
|
|
- initTag(true);
|
|
|
- initReticleMeshs(true);
|
|
|
+ handleTag(true);
|
|
|
+ handleReticleMeshs(true);
|
|
|
}
|
|
|
|
|
|
cb && cb();
|
|
@@ -608,11 +746,43 @@ const IndexPage: FC = observer(() => {
|
|
|
*/
|
|
|
const backForSeparate = () => {
|
|
|
modelSingleClick("", () => {
|
|
|
+ renderModel.setAutoRotate(ROTATE_TYPE.TRUE);
|
|
|
renderModel.setControlsStatus(true, true, false, true, false);
|
|
|
setModelState(MODEL_STATE.ZOOM_UP);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+ const initOutlinePass = () => {
|
|
|
+ // 创建一个EffectComposer(效果组合器)对象,然后在该对象上添加后期处理通道
|
|
|
+ effectCompose.current = new EffectComposer(renderModel.renderer);
|
|
|
+ // 新建一个场景通道 为了覆盖到原来的场景上
|
|
|
+ const renderPass = new RenderPass(renderModel.scene, renderModel.camera);
|
|
|
+ effectCompose.current.addPass(renderPass);
|
|
|
+ // 创建物体边缘发光通道
|
|
|
+ outlinePass.current = new OutlinePass(
|
|
|
+ new Vector2(renderModel.rect?.width, renderModel.rect?.height),
|
|
|
+ renderModel.scene,
|
|
|
+ renderModel.camera
|
|
|
+ );
|
|
|
+ outlinePass.current.usePatternTexture = false; // 是否使用父级的材质
|
|
|
+ outlinePass.current.edgeStrength = 2; // 边框的亮度
|
|
|
+ outlinePass.current.edgeGlow = 1; // 光晕
|
|
|
+ outlinePass.current.pulsePeriod = 0; // 呼吸闪烁的速度
|
|
|
+ outlinePass.current.visibleEdgeColor = new Color(1, 1, 1); // 呼吸显示的颜色
|
|
|
+ outlinePass.current.hiddenEdgeColor = new Color(0.1, 0.04, 0.02); // 呼吸消失的颜色
|
|
|
+ outlinePass.current.edgeThickness = 1; // 边框宽度
|
|
|
+ outlinePass.current.downSampleRatio = 2; // 边框弯曲度
|
|
|
+ effectCompose.current.addPass(outlinePass.current);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleBack = () => {
|
|
|
+ if (isSingleModel) {
|
|
|
+ backForSeparate();
|
|
|
+ } else if (isWen) {
|
|
|
+ wenliHandler(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
return (
|
|
|
<View className="page">
|
|
|
<CanvasAdapter
|
|
@@ -628,8 +798,8 @@ const IndexPage: FC = observer(() => {
|
|
|
/>
|
|
|
|
|
|
<View className="toolbar">
|
|
|
- {isSingleModel ? (
|
|
|
- <Button className="btn" onClick={backForSeparate}>
|
|
|
+ {isSingleModel || isWen ? (
|
|
|
+ <Button className="btn" onClick={handleBack}>
|
|
|
返回
|
|
|
</Button>
|
|
|
) : (
|