index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. import { useEffect, useMemo, useRef, useState } from "react";
  2. import { Button, View } from "@tarojs/components";
  3. import Taro, { FC } from "@tarojs/taro";
  4. import { observer } from "mobx-react";
  5. import {
  6. ROTATE_TYPE,
  7. renderModel,
  8. ShadowModel,
  9. ReticleModel,
  10. TagModelParams,
  11. TagModel,
  12. } from "../../models";
  13. import {
  14. Clock,
  15. GammaEncoding,
  16. LinearFilter,
  17. Mesh,
  18. MeshStandardMaterial,
  19. PointLight,
  20. Vector2,
  21. Vector3,
  22. } from "three-platformize";
  23. import { Easing, Tween } from "@tweenjs/tween.js";
  24. import { cancelAnimationFrame, requestAnimationFrame } from "@tarojs/runtime";
  25. import { CanvasAdapter } from "../../components";
  26. import "./index.scss";
  27. enum MODEL_STATE {
  28. DEFAULT = 0,
  29. ZOOM_UP = 1,
  30. ZOOM_UP_CLICK = 2,
  31. }
  32. interface TweenHandlerOptions<T extends object> {
  33. /** 当前属性 */
  34. curProps: T;
  35. /** 目标属性 */
  36. targetProps: T;
  37. /** 动画更新回调 */
  38. onUpdate?: (e: T) => void;
  39. /** 动画完成回调 */
  40. cb?: Function;
  41. /** 动画时长 默认 1000 ms */
  42. delay?: number;
  43. }
  44. const DEFUALT_SCALE = 20;
  45. const DENGZHAO_MIN_ROTATE = -1.2;
  46. const DENGZHAO_MAX_ROTATE = -2.55;
  47. const system = Taro.getSystemInfoSync();
  48. const IndexPage: FC = observer(() => {
  49. const clock = useRef(new Clock());
  50. const bulbLight = useRef<PointLight>();
  51. const cameraPosition = useRef(new Vector3(0, 100, 360));
  52. /**
  53. * 底部阴影
  54. */
  55. const shadowPlan = useRef<Mesh>();
  56. const [modelState, setModelState] = useState<MODEL_STATE>(
  57. MODEL_STATE.DEFAULT
  58. );
  59. /**
  60. * 是否分解
  61. */
  62. const isSeparate = useMemo(
  63. () => modelState === MODEL_STATE.ZOOM_UP,
  64. [modelState]
  65. );
  66. /**
  67. * 是否单独展示某个模型
  68. */
  69. const isSingleModel = useMemo(
  70. () => modelState === MODEL_STATE.ZOOM_UP_CLICK,
  71. [modelState]
  72. );
  73. const hotSeparateAnimArr = useRef<ReticleModel[]>([]);
  74. const tagArr = useRef<TagModel[]>([]);
  75. const startMouse = useRef(new Vector2(0, 0));
  76. const mouseV2 = useRef(new Vector2(0, 0));
  77. useEffect(() => {
  78. setTimeout(async () => {
  79. await init();
  80. animate();
  81. }, 100);
  82. return renderModel.dispose;
  83. }, []);
  84. const init = async () => {
  85. await renderModel.init("#wgl");
  86. // 创建点光源
  87. bulbLight.current = new PointLight(16772744, 2.5, 50, 2);
  88. bulbLight.current.position.set(1.45, -2.95, -1.17);
  89. bulbLight.current.castShadow = true;
  90. bulbLight.current.visible = false;
  91. renderModel.scene.add(bulbLight.current);
  92. renderModel.setControlsStatus(true, true, false);
  93. handleControlsAngle(true);
  94. renderModel.camera.position.copy(cameraPosition.current);
  95. await renderModel.loadModel({
  96. filePath:
  97. "https://houseoss.4dkankan.com/project/yzdyh-dadu/test/model.FBX",
  98. fileType: "fbx",
  99. });
  100. renderModel.modelMaterialList.forEach((mesh) => {
  101. const meshStandardMaterial = new MeshStandardMaterial();
  102. // 设置物体是否投射阴影
  103. mesh.castShadow = true;
  104. // 设置物体是否接收阴影
  105. mesh.receiveShadow = true;
  106. meshStandardMaterial.copy(mesh.material);
  107. // 设置材质透明度
  108. meshStandardMaterial.opacity = 0.01;
  109. // 告诉 three 库该材质渲染为半透明
  110. meshStandardMaterial.transparent = true;
  111. // 设置材质渲染面,2:双面渲染;1:只渲染正面;0:只渲染背面
  112. meshStandardMaterial.side = 2;
  113. switch (mesh.name) {
  114. case "dengzhao1":
  115. case "dengzhao2":
  116. case "dengpan":
  117. setModelMaterial(meshStandardMaterial, "dengzhao");
  118. break;
  119. case "dengguan":
  120. setModelMaterial(meshStandardMaterial, "dengguan");
  121. break;
  122. case "tongniudengdizuo":
  123. setModelMaterial(meshStandardMaterial, "tongniudengdizuo");
  124. break;
  125. }
  126. // 设置材质粗糙度
  127. meshStandardMaterial.roughness = 0.9;
  128. // 设置材质金属度
  129. meshStandardMaterial.metalness = 0.3;
  130. // 设置材质纹理过滤器
  131. meshStandardMaterial.map!.minFilter = LinearFilter;
  132. meshStandardMaterial.map!.magFilter = LinearFilter;
  133. // 设置材质纹理编码方式
  134. meshStandardMaterial.map!.encoding = GammaEncoding;
  135. mesh.material.dispose();
  136. mesh.material = meshStandardMaterial;
  137. mesh.material.needsUpdate = true;
  138. });
  139. setTimeout(() => {
  140. shadowPlaneShow();
  141. modelChildRotation(5);
  142. tweenHandler({
  143. curProps: {
  144. x: 0,
  145. },
  146. targetProps: {
  147. x: 20,
  148. },
  149. onUpdate: (e) => {
  150. renderModel.model?.scale.copy(new Vector3(e.x, e.x, e.x));
  151. },
  152. cb: () => {
  153. setTimeout(() => {
  154. renderModel.setAutoRotate(ROTATE_TYPE.TRUE);
  155. }, 800);
  156. },
  157. });
  158. }, 300);
  159. };
  160. const setModelMaterial = (
  161. meshStandardMaterial: MeshStandardMaterial,
  162. name: string
  163. ) => {
  164. renderModel.setModelMap(
  165. meshStandardMaterial,
  166. require(`./resource/${name}_D.jpg`)
  167. );
  168. renderModel.setModelRoughness(
  169. meshStandardMaterial,
  170. require(`./resource/${name}_S.jpg`)
  171. );
  172. renderModel.setModelMetalness(
  173. meshStandardMaterial,
  174. require(`./resource/${name}_R.jpg`)
  175. );
  176. renderModel.setModelEmissive(
  177. meshStandardMaterial,
  178. require(`./resource/${name}_D.png`)
  179. );
  180. };
  181. /**
  182. * 相机动画
  183. */
  184. function tweenHandler<T extends object>(options: TweenHandlerOptions<T>) {
  185. let animaId: number | NodeJS.Timeout = 0;
  186. const { curProps, targetProps, onUpdate, cb } = options;
  187. const tween = new Tween(curProps)
  188. .to(targetProps, options.delay || 1000)
  189. .onUpdate(onUpdate)
  190. .onComplete(() => {
  191. cancelAnimationFrame(animaId as number);
  192. cb && cb();
  193. })
  194. .easing(Easing.Quintic.Out)
  195. .start();
  196. function animate(time?: number) {
  197. animaId = requestAnimationFrame(animate);
  198. tween.update(time);
  199. }
  200. animate();
  201. }
  202. const modelChildRotation = (e: number) => {
  203. const dengzhao1Mesh = renderModel.model?.getObjectByName("dengzhao1");
  204. if (dengzhao1Mesh) {
  205. dengzhao1Mesh.rotation.z =
  206. DENGZHAO_MIN_ROTATE +
  207. (DENGZHAO_MAX_ROTATE - DENGZHAO_MIN_ROTATE) * (e - 1) * 0.1;
  208. renderModel.modelMaterialList.forEach((mesh) => {
  209. if (e > 5) {
  210. const t = 1 - 10 * (e - 8) * (1 / 30);
  211. if (e >= 8) {
  212. mesh.material.opacity = t;
  213. }
  214. mesh.visible = !(t <= 0.01);
  215. } else {
  216. mesh.material.opacity = 1;
  217. mesh.visible = true;
  218. }
  219. });
  220. }
  221. };
  222. /**
  223. * 是否展示底部阴影
  224. */
  225. const shadowPlaneShow = (show = true) => {
  226. if (!shadowPlan.current && renderModel.model) {
  227. shadowPlan.current = new ShadowModel(
  228. "https://houseoss.4dkankan.com/project/yzdyh-dadu/test/shadow.png",
  229. 1024,
  230. 1024,
  231. { x: Math.PI / 2, y: Math.PI / 80, z: -Math.PI / 2 },
  232. new Vector3(0, -2.35, 0),
  233. 0.0055,
  234. renderModel.model
  235. ).mesh;
  236. }
  237. shadowPlan.current!.visible = show;
  238. };
  239. /**
  240. * 拆解/合并模型
  241. */
  242. const handleSeparate = () => {
  243. handleControlsAngle(isSeparate);
  244. renderModel.setAutoRotate(ROTATE_TYPE.FALSE);
  245. renderModel.setControlsStatus(false, false, false);
  246. if (!isSeparate) {
  247. handleControlsDistance(320);
  248. shadowPlaneShow(false);
  249. const pos = renderModel.camera.position.clone();
  250. const lookAt = renderModel.controls.target.clone();
  251. const targetPos = new Vector3(0, 0, 320);
  252. const targetLookAt = new Vector3(0, 0, 0);
  253. tweenHandler({
  254. curProps: {
  255. x1: pos.x,
  256. y1: pos.y,
  257. z1: pos.z,
  258. x2: lookAt.x,
  259. y2: lookAt.y,
  260. z2: lookAt.z,
  261. },
  262. targetProps: {
  263. x1: targetPos.x,
  264. y1: targetPos.y,
  265. z1: targetPos.z,
  266. x2: targetLookAt.x,
  267. y2: targetLookAt.y,
  268. z2: targetLookAt.z,
  269. },
  270. onUpdate: (e) => {
  271. renderModel.camera.position.set(e.x1, e.y1, e.z1);
  272. renderModel.controls.target.set(e.x2, e.y2, e.z2);
  273. renderModel.controls.update();
  274. },
  275. cb: () => {
  276. handleModelSeparateAnimation(true, () => {
  277. renderModel.setControlsStatus(true, true, false, true, false);
  278. renderModel.setAutoRotate(ROTATE_TYPE.TRUE);
  279. setModelState(MODEL_STATE.ZOOM_UP);
  280. });
  281. },
  282. });
  283. } else {
  284. handleModelSeparateAnimation(false, () => {
  285. revertModel(() => {
  286. renderModel.setControlsStatus(true, true, false);
  287. renderModel.setAutoRotate(ROTATE_TYPE.TRUE);
  288. shadowPlaneShow(true);
  289. setModelState(MODEL_STATE.DEFAULT);
  290. });
  291. });
  292. }
  293. };
  294. /**
  295. * 恢复模型
  296. */
  297. const revertModel = (cb?: Function) => {
  298. const pos = renderModel.camera.position.clone();
  299. const lookAt = renderModel.controls.target.clone();
  300. const targetPos = new Vector3(0, 30, 372);
  301. const targetLookAt = new Vector3(0, 0, 0);
  302. tweenHandler({
  303. curProps: {
  304. x1: pos.x,
  305. y1: pos.y,
  306. z1: pos.z,
  307. x2: lookAt.x,
  308. y2: lookAt.y,
  309. z2: lookAt.z,
  310. },
  311. targetProps: {
  312. x1: targetPos.x,
  313. y1: targetPos.y,
  314. z1: targetPos.z,
  315. x2: targetLookAt.x,
  316. y2: targetLookAt.y,
  317. z2: targetLookAt.z,
  318. },
  319. onUpdate: (e) => {
  320. renderModel.camera.position.set(e.x1, e.y1, e.z1);
  321. renderModel.controls.target.set(e.x2, e.y2, e.z2);
  322. renderModel.controls.update();
  323. },
  324. cb,
  325. });
  326. };
  327. const handleControlsDistance = (dis: number, maxDis = 470) => {
  328. renderModel.controls.minDistance = dis;
  329. renderModel.controls.maxDistance = maxDis;
  330. };
  331. /**
  332. * 修改控制器垂直最大角度
  333. */
  334. const handleControlsAngle = (type: boolean) => {
  335. renderModel.controls.maxPolarAngle = type ? 0.59 * Math.PI : Math.PI;
  336. };
  337. /**
  338. * 模型分离动画
  339. * @param type 是否分离
  340. */
  341. const handleModelSeparateAnimation = (type: boolean, cb?: Function) => {
  342. let loadCount = 0;
  343. renderModel.modelMaterialList.forEach((item) => {
  344. if (type) {
  345. const pos = item.position.clone();
  346. switch (item.name) {
  347. case "dengguan":
  348. pos.add(new Vector3(0, 1, 0));
  349. break;
  350. case "dengpan":
  351. pos.add(new Vector3(0, -0.5, 0));
  352. break;
  353. case "dengzhao1":
  354. pos.add(new Vector3(-0.3, 0.2, 0));
  355. break;
  356. case "dengzhao2":
  357. pos.add(new Vector3(0.3, 0.2, 0));
  358. break;
  359. case "tongniudengdizuo":
  360. pos.add(new Vector3(0, -1, 0));
  361. }
  362. tweenHandler({
  363. curProps: item.position,
  364. targetProps: pos,
  365. cb: () => {
  366. loadCount++;
  367. if (loadCount === 5) {
  368. initTag(true);
  369. initReticleMeshs(true);
  370. cb && cb();
  371. }
  372. },
  373. });
  374. } else {
  375. initTag(false);
  376. initReticleMeshs(false);
  377. tweenHandler({
  378. curProps: item.position,
  379. targetProps: item._position,
  380. cb,
  381. });
  382. }
  383. });
  384. };
  385. const initTag = (visible: boolean) => {
  386. if (!tagArr.current.length) {
  387. const temp: TagModel[] = [];
  388. const tagStack: TagModelParams[] = [
  389. {
  390. url: require("./resource/daoyanguan.png"),
  391. maskUrl: require("./resource/biaoqian_left.png"),
  392. width: 256,
  393. height: 36,
  394. position: new Vector3(1.1, 3.1, 0),
  395. scale: 0.007,
  396. rotation: { x: 0, y: 0, z: 0 },
  397. },
  398. {
  399. url: require("./resource/denggai.png"),
  400. maskUrl: require("./resource/biaoqian_left.png"),
  401. width: 256,
  402. height: 36,
  403. position: new Vector3(1.7, 2.4, 0),
  404. scale: 0.005,
  405. rotation: { x: 0, y: 0, z: 0 },
  406. },
  407. ];
  408. tagStack.forEach((tag) => {
  409. const tagIns = new TagModel(tag);
  410. temp.push(tagIns);
  411. renderModel.model?.add(tagIns.mesh);
  412. });
  413. tagArr.current.push(...temp);
  414. }
  415. tagArr.current.forEach((tag) => {
  416. visible ? tag.show() : tag.hide();
  417. });
  418. };
  419. const initReticleMeshs = (visible: boolean) => {
  420. if (!hotSeparateAnimArr.current.length) {
  421. const temp: ReticleModel[] = [];
  422. const vectorStack = [
  423. new Vector3(0, 2.7, 0.65),
  424. new Vector3(0, 0.9, 0.65),
  425. new Vector3(0, -0.7, 0.65),
  426. new Vector3(0, -2, 0.65),
  427. ];
  428. vectorStack.forEach((vector) => {
  429. const reticle = new ReticleModel({
  430. url: require("./resource/reticle-animation.png"),
  431. planeWidth: 150,
  432. tilesHorizontal: 50,
  433. tilesVertical: 1,
  434. tileDisplayDuration: 50,
  435. scale: 0.0022,
  436. position: vector,
  437. camera: renderModel.camera,
  438. });
  439. renderModel.model?.add(reticle.reticleAnim);
  440. temp.push(reticle);
  441. });
  442. hotSeparateAnimArr.current.push(...temp);
  443. }
  444. hotSeparateAnimArr.current.forEach((item) => {
  445. visible ? item.show() : item.hide();
  446. });
  447. };
  448. const animate = () => {
  449. const time = clock.current.getDelta();
  450. hotSeparateAnimArr.current.forEach((item) => {
  451. item.update(time);
  452. });
  453. requestAnimationFrame(animate);
  454. };
  455. const clickHandler = (x: number, y: number) => {
  456. const v2 = new Vector2(x, y);
  457. // 比较两个向量是否相等
  458. if (v2.equals(startMouse.current)) {
  459. setCanvasPosition(x, y);
  460. const res = renderModel.mouseRaycaster(mouseV2.current);
  461. for (let i = 0; i < res.length; i++) {
  462. const name = res[i].object.name;
  463. if (!name) continue;
  464. console.log("click model:", name);
  465. if (
  466. isSeparate &&
  467. res[i].object.visible &&
  468. res[i].object.parent?.visible
  469. ) {
  470. handleModelZoomUp(name);
  471. break;
  472. }
  473. }
  474. }
  475. };
  476. /**
  477. * 单独展示某个模型
  478. */
  479. const handleModelZoomUp = (name: string) => {
  480. renderModel.setAutoRotate(ROTATE_TYPE.FALSE);
  481. modelSingleClick(name, () => {
  482. renderModel.setControlsStatus(false, true, false, true, false);
  483. setModelState(MODEL_STATE.ZOOM_UP_CLICK);
  484. });
  485. };
  486. const modelSingleClick = (name: string, cb: Function) => {
  487. let targetProps = {
  488. x: 0,
  489. y: 0,
  490. z: 0,
  491. scale: DEFUALT_SCALE,
  492. };
  493. switch (name) {
  494. case "dengguan":
  495. targetProps = {
  496. x: 0,
  497. y: -68,
  498. z: 0,
  499. scale: 0.6 * DEFUALT_SCALE,
  500. };
  501. break;
  502. }
  503. if (name) {
  504. renderModel.modelMaterialList.forEach((model) => {
  505. model.visible = model.name.includes(name);
  506. });
  507. }
  508. initTag(false);
  509. initReticleMeshs(false);
  510. tweenHandler({
  511. curProps: {
  512. x: renderModel.model!.position.x,
  513. y: renderModel.model!.position.y,
  514. z: renderModel.model!.position.z,
  515. scale: renderModel.model!.scale.x,
  516. },
  517. targetProps,
  518. onUpdate: (e) => {
  519. renderModel.model?.position.set(e.x, e.y, e.z);
  520. renderModel.model?.scale.set(e.scale, e.scale, e.scale);
  521. },
  522. cb: () => {
  523. if (name === "") {
  524. renderModel.modelMaterialList.forEach((model) => {
  525. model.visible = true;
  526. });
  527. initTag(true);
  528. initReticleMeshs(true);
  529. }
  530. cb && cb();
  531. },
  532. });
  533. };
  534. const setCanvasPosition = (x: number, y: number) => {
  535. mouseV2.current.x = (x / system.windowWidth) * 2 - 1;
  536. mouseV2.current.y = (-y / system.windowHeight) * 2 + 1;
  537. };
  538. /**
  539. * 返回分解模型
  540. */
  541. const backForSeparate = () => {
  542. modelSingleClick("", () => {
  543. renderModel.setControlsStatus(true, true, false, true, false);
  544. setModelState(MODEL_STATE.ZOOM_UP);
  545. });
  546. };
  547. return (
  548. <View className="page">
  549. <CanvasAdapter
  550. onTouchStart={(e) => {
  551. // @ts-ignore
  552. startMouse.current = new Vector2(e.clientX, e.clientY);
  553. renderModel.setAutoRotate(ROTATE_TYPE.DELAY);
  554. }}
  555. onTouchEnd={(e) => {
  556. // @ts-ignore
  557. clickHandler(e.clientX, e.clientY);
  558. }}
  559. />
  560. <View className="toolbar">
  561. {isSingleModel ? (
  562. <Button className="btn" onClick={backForSeparate}>
  563. 返回
  564. </Button>
  565. ) : (
  566. <Button className="btn" onClick={handleSeparate}>
  567. {!isSeparate ? "拆解" : "合并"}
  568. </Button>
  569. )}
  570. </View>
  571. </View>
  572. );
  573. });
  574. export default IndexPage;