├── src ├── vite-env.d.ts ├── global.d.ts ├── assets │ └── styles │ │ ├── index.css │ │ └── pickColor.css ├── shim-vue.d.ts ├── http │ ├── api │ │ └── common.ts │ └── request.ts ├── App.vue ├── router │ └── index.ts ├── main.ts ├── stores │ ├── index.ts │ └── three.ts ├── types │ └── index.ts ├── components │ ├── left │ │ └── index.vue │ ├── ProNumberInput │ │ └── index.vue │ ├── TextureEditor │ │ └── index.vue │ ├── center │ │ └── index.vue │ └── right │ │ └── index.vue ├── views │ └── index.vue └── utils │ └── threeUtils.ts ├── .vscode └── extensions.json ├── postcss.config.js ├── process.config.js ├── public ├── low_poly_character_swordsman.glb └── vite.svg ├── tailwind.config.js ├── tsconfig.node.json ├── .babelrc ├── .gitignore ├── .prettierrc.cjs ├── README.md ├── index.html ├── tsconfig.json ├── components.d.ts ├── package.json ├── vite.config.ts ├── .eslintrc.js └── auto-imports.d.ts /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | window: any; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /process.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/low_poly_character_swordsman.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderZqs/gltf-editor/HEAD/public/low_poly_character_swordsman.glb -------------------------------------------------------------------------------- /src/shim-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import type { DefineComponent } from "vue"; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /src/http/api/common.ts: -------------------------------------------------------------------------------- 1 | import { request } from "../request"; 2 | 3 | export function uploadFile(data) { 4 | return request({ 5 | url: "/common/upload", 6 | method: "post", 7 | data, 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | purge: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], 4 | content: [], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": [ 4 | [ 5 | "import", 6 | { 7 | "libraryName": "ant-design-vue", 8 | "libraryDirectory": "es", 9 | "style": true // style: true 时加载的是less文件 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, createWebHistory } from "vue-router"; 2 | 3 | const ConstantRouterMap = [ 4 | { 5 | path: "/", 6 | component: () => import("@/views/index.vue"), 7 | }, 8 | ]; 9 | 10 | const router = createRouter({ 11 | history: createWebHashHistory(), 12 | routes: [...ConstantRouterMap], 13 | }); 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, // tab 使用两个空格 3 | endOfLine: "auto", // 保持现有的行尾 4 | useTabs: false, // 不使用制表符缩进,使用空格缩进 5 | semi: true, // 代码需要分号结尾 6 | singleQuote: false, // 单引号 7 | bracketSpacing: true, // 对象左右两侧需要空格 8 | jsxBracketSameLine: false, // html 关闭标签换行 9 | arrowParens: "avoid", // 单参数的箭头函数参数不需要括号 10 | proseWrap: "never", // markdown文档不换行 11 | trailingComma: "none" // 结尾处不加逗号 12 | }; 13 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import "tailwindcss/tailwind.css"; 2 | import { createApp } from "vue"; 3 | import App from "./App.vue"; 4 | import { createPinia } from "pinia"; 5 | import router from "@/router/index"; 6 | import "./assets/styles/index.css"; 7 | 8 | import ColorPicker from "colorpicker-v3"; // 注册组件 9 | import "./assets/styles/pickColor.css"; // 引入样式文件 10 | 11 | const app = createApp(App); 12 | app.use(ColorPicker).use(createPinia()).use(router).mount("#app"); 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # glTF 编辑器 2 | 3 | #### 该项目是一款简易版的glTF编辑器,拥有上传、编辑模型材质、位移、旋转等与下载模型功能。 4 | 5 | ## 技术栈 6 | 7 | - 前端框架:Vue3 + pinia 8 | - 3D框架:threeJS 9 | - UI框架:ant-design-vue 10 | 11 | ## 编辑功能支持 12 | + [x] 位移 13 | + [x] 旋转 14 | + [x] 缩放 15 | + [x] 颜色 16 | + [x] 粗糙度 17 | + [x] 金属度 18 | + [x] 贴图 19 | + [x] 自发光贴图 20 | + [x] 透明贴图 21 | + [x] 凹凸贴图 22 | + [x] 法线贴图 23 | + [x] 位移贴图 24 | + [x] 粗糙贴图 25 | + [x] 金属贴图 26 | + [x] 面 27 | + [x] 混合模式 28 | + [x] 透明度 29 | 30 | ## 安装 31 | 32 | ### 安装依赖 33 | ``` 34 | npm install 35 | ``` 36 | ### 本地运行 37 | ``` 38 | npm run dev 39 | ``` 40 | 41 | ### 打包 42 | ``` 43 | npm run build 44 | ``` -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GLTF 编辑器 8 | 9 | 10 |
11 | 12 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref } from "vue"; 2 | import { Mesh, Tree } from "../types/index"; 3 | 4 | import { defineStore } from "pinia"; 5 | export default defineStore("index", () => { 6 | let current: Ref = ref({}); 7 | let tree: Ref = ref({} as Tree); 8 | 9 | /** 10 | * 获得tree 11 | * @param model 12 | * @param ls 13 | */ 14 | 15 | const getTree = (model, ls) => { 16 | if (model.children && model.children.length) { 17 | if (model.children.some(v => v.isMesh)) { 18 | model.children.forEach(e => { 19 | if (e.isMesh) { 20 | ls.push({ uuid: e.uuid, title: e.name, key: e.uuid, children: [] }); 21 | } 22 | 23 | getTree(e, ls[ls.length - 1]?.children); 24 | }); 25 | } else { 26 | model.children.forEach(e => { 27 | getTree(e, ls); 28 | }); 29 | } 30 | } 31 | }; 32 | 33 | return { current, tree, getTree }; 34 | }); 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "allowJs": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "experimentalDecorators": true, 14 | "useDefineForClassFields": true, 15 | "noImplicitAny": false, 16 | "sourceMap": true, 17 | "baseUrl": ".", 18 | "types": ["webpack-env", "node"], 19 | "paths": { 20 | "@/*": ["src/*"] 21 | }, 22 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 23 | }, 24 | "include": [ 25 | "src/**/*.ts", 26 | "src/**/*.tsx", 27 | "src/**/*.vue", 28 | "tests/**/*.ts", 29 | "tests/**/*.tsx", 30 | "src/views/home/hooks/bg.js", 31 | "src/views/home/hooks/bg.js", 32 | "auto-imports.d.ts" 33 | ], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AButton: typeof import('ant-design-vue/lib')['Button'] 11 | AConfigProvider: typeof import('ant-design-vue/lib')['ConfigProvider'] 12 | ASpin: typeof import('ant-design-vue/lib')['Spin'] 13 | Center: typeof import('./src/components/center/index.vue')['default'] 14 | Left: typeof import('./src/components/left/index.vue')['default'] 15 | ProNumberInput: typeof import('./src/components/ProNumberInput/index.vue')['default'] 16 | Right: typeof import('./src/components/right/index.vue')['default'] 17 | RouterLink: typeof import('vue-router')['RouterLink'] 18 | RouterView: typeof import('vue-router')['RouterView'] 19 | TextureEditor: typeof import('./src/components/TextureEditor/index.vue')['default'] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | type Mesh = { 2 | type: string; 3 | name: string; 4 | visible: boolean; 5 | position: THREE.Vector3; 6 | scale: THREE.Vector3; 7 | rotate: THREE.Vector3; 8 | color: THREE.Color; 9 | roughness: number; 10 | shininess: number; 11 | vectorColor: THREE.Vector3; 12 | side: THREE.Side; 13 | opacity: number; 14 | emissive: number; 15 | children: Mesh[]; 16 | }; 17 | 18 | type Tree = { 19 | uuid: string; 20 | title: string; 21 | children: Tree[]; 22 | }; 23 | 24 | type Config = { 25 | type: string; 26 | name: string; 27 | vertexCount: number; 28 | color: THREE.Color; 29 | triangleCount: number; 30 | position: THREE.Vector3; 31 | scale: THREE.Vector3; 32 | rotation: { x: number; y: number; z: number }; 33 | side: string; 34 | blending: string; 35 | opacity: number; 36 | roughness: number; 37 | metalness: number; 38 | map: string; 39 | emissiveMap: string; 40 | vertexColors: boolean; 41 | alphaMap: string; 42 | bumpMap: string; 43 | normalMap: string; 44 | displacementMap: string; 45 | roughnessMap: string; 46 | metalnessMap: string; 47 | }; 48 | 49 | export type { Mesh, Tree, Config }; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gltf-editor", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@types/three": "^0.157.0", 13 | "@vitejs/plugin-vue-jsx": "^3.0.2", 14 | "ant-design-vue": "^4.0.3", 15 | "axios": "^1.3.4", 16 | "colorpicker-v3": "^2.10.2", 17 | "consola": "^3.2.3", 18 | "file-saver": "^2.0.5", 19 | "gsap": "^3.12.2", 20 | "less": "^4.2.0", 21 | "pinia": "^2.1.7", 22 | "rollup-plugin-visualizer": "^5.9.2", 23 | "sass": "^1.69.4", 24 | "simplex-noise": "^4.0.1", 25 | "three": "^0.148.0", 26 | "three-js-csg": "^72.0.0", 27 | "unplugin-auto-import": "^0.16.7", 28 | "unplugin-vue-components": "^0.25.2", 29 | "vite-plugin-cdn-import": "^0.3.5", 30 | "vite-plugin-compression": "^0.5.1", 31 | "vite-plugin-style-import": "^2.0.0", 32 | "vue": "^3.3.4", 33 | "vue-router": "^4.2.5" 34 | }, 35 | "devDependencies": { 36 | "@vitejs/plugin-vue": "^4.4.0", 37 | "autoprefixer": "^10.4.16", 38 | "postcss": "^8.4.31", 39 | "tailwindcss": "^3.3.3", 40 | "typescript": "^5.0.2", 41 | "vite": "^4.4.5", 42 | "vue-tsc": "^1.8.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/left/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/ProNumberInput/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 55 | 56 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "node:url"; 2 | import { defineConfig } from "vite"; 3 | import vue from "@vitejs/plugin-vue"; 4 | import { visualizer } from "rollup-plugin-visualizer"; 5 | import { autoComplete, Plugin as importToCDN } from "vite-plugin-cdn-import"; 6 | import vueJsx from "@vitejs/plugin-vue-jsx"; 7 | import compressPlugin from "vite-plugin-compression"; 8 | import Components from "unplugin-vue-components/vite"; 9 | import AutoImport from "unplugin-auto-import/vite"; 10 | 11 | let cdn = () => 12 | importToCDN({ 13 | modules: [ 14 | { 15 | name: "three", 16 | var: "THREE", 17 | path: `https://cdn.bootcdn.net/ajax/libs/three.js/0.148.0/three.js` 18 | }, 19 | ] 20 | }); 21 | 22 | export default defineConfig({ 23 | base: process.env.NODE_ENV === "development" ? "" : "./", 24 | plugins: [ 25 | vue(), 26 | vueJsx(), 27 | visualizer(), 28 | cdn(), 29 | compressPlugin({ 30 | threshold: 100000 // 对大于 1mb 的文件进行压缩 31 | }), 32 | AutoImport({ 33 | imports: ["vue", "vue-router"], 34 | eslintrc: { 35 | enabled: false, 36 | filepath: "./.eslintrc-auto-import.json", 37 | globalsPropValue: true 38 | } 39 | }) 40 | ], 41 | build: { 42 | target: ["edge90", "chrome90", "firefox90", "safari15"] 43 | }, 44 | resolve: { 45 | alias: { 46 | "@": fileURLToPath(new URL("./src", import.meta.url)) 47 | } 48 | }, 49 | server: { 50 | port: 8888, 51 | host: "0.0.0.0", 52 | proxy: { 53 | "/api": { 54 | target: "http://www.hiwindy.cn", 55 | changeOrigin: true, 56 | rewrite: path => path.replace(/^\/api/, "") 57 | } 58 | } 59 | }, 60 | 61 | css: { 62 | preprocessorOptions: { 63 | less: { 64 | javascriptEnabled: true, 65 | modifyVars: { 66 | "primary-color": "#000000", 67 | "link-color": "#000000" 68 | } 69 | } 70 | } 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /src/http/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from "axios"; 2 | import { message } from "ant-design-vue"; 3 | 4 | const service = axios.create({ 5 | baseURL: "/life_photo", 6 | timeout: 5000, 7 | withCredentials: true, 8 | }); 9 | 10 | // Request interceptors 11 | service.interceptors.request.use( 12 | (config) => { 13 | return config; 14 | }, 15 | (error) => { 16 | Promise.reject(error); 17 | } 18 | ); 19 | 20 | /** 21 | * 请求 22 | * @param params 参数 23 | */ 24 | export function request(config: AxiosRequestConfig): Promise { 25 | const args = Object.assign({}, config); 26 | return new Promise(function (resolve, reject) { 27 | service 28 | .request(config) 29 | .then((res) => { 30 | const { 31 | status, 32 | statusText, 33 | data: { code, msg, data, success }, 34 | } = res; 35 | 36 | if (status === 200) { 37 | if (success) { 38 | return resolve(data); 39 | } else { 40 | console.log(code, msg); 41 | message.error('msg || "网络异常"'); 42 | return reject(new Error(msg || "服务器端错误")); 43 | } 44 | } 45 | }) 46 | .catch((err) => { 47 | if (!err.response) { 48 | return reject(new Error("网络错误:" + err.message)); 49 | } 50 | 51 | const { 52 | status, 53 | statusText, 54 | data: { code, msg, data, success }, 55 | } = err.response; 56 | if (status === 401) { 57 | } else if (status === 403) { 58 | message.error({ 59 | content: msg || "没有授权", 60 | type: "error", 61 | duration: 5 * 1000, 62 | }); 63 | return reject(msg); 64 | } else if (status === 500) { 65 | message.error({ 66 | content: msg || "网络异常, 请稍后再试", 67 | type: "error", 68 | duration: 5 * 1000, 69 | }); 70 | return reject(msg); 71 | } else if (status === 502) { 72 | message.error({ 73 | content: "服务器故障, 请稍后再试", 74 | type: "error", 75 | duration: 5 * 1000, 76 | }); 77 | return reject("服务器故障, 请稍后再试"); 78 | } else { 79 | message.error({ 80 | content: statusText || "网络异常", 81 | type: "error", 82 | duration: 5 * 1000, 83 | }); 84 | return reject(new Error(statusText || "网络异常")); 85 | } 86 | }); 87 | }); 88 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 官网: https://cn.eslint.org/docs/user-guide/getting-started 3 | * 规则查阅:https://cn.eslint.org/docs/rules/ 4 | * 参考资料: 5 | http://tech.tea-culture.top/code/eslint/#eslint-%E8%A7%84%E5%88%99%E6%80%BB%E8%A7%88 6 | https://blog.csdn.net/image_fzx/article/details/118195141 7 | https://blog.csdn.net/weixin_57649833/article/details/120757938 8 | */ 9 | module.exports = { 10 | root: true, 11 | env: { 12 | node: true 13 | }, 14 | extends: [ 15 | "plugin:vue/vue3-essential", 16 | "@vue/standard", 17 | "@vue/typescript/recommended" 18 | ], 19 | parserOptions: { 20 | ecmaVersion: 2020 21 | }, 22 | rules: { 23 | "vue/attribute-hyphenation": 0, 24 | // 自定义组件名称 - 驼峰和连字符 25 | "vue/component-definition-name-casing": 0, 26 | // html 闭括号-换行 27 | "vue/html-closing-bracket-newline": [ 28 | 2, 29 | { 30 | singleline: "never", 31 | multiline: "always" 32 | } 33 | ], 34 | // html 闭括号之前无空格 35 | "vue/html-closing-bracket-spacing": 2, 36 | // html 需要有结束标签,除了自闭合标签 37 | "vue/html-end-tags": 2, 38 | // 缩进html 39 | "vue/html-indent": [ 40 | "error", 41 | 4, 42 | { 43 | attribute: 1, 44 | baseIndent: 1, 45 | closeBracket: 0, 46 | alignAttributesVertically: true, 47 | ignores: [] 48 | } 49 | ], 50 | "vue/max-attributes-per-line": [ 51 | 2, 52 | { 53 | singleline: 4, 54 | multiline: 4 55 | } 56 | ], 57 | // 禁止组件已注册但未使用的情况 58 | "vue/no-unused-components": [2], 59 | "no-multiple-empty-lines": 2, 60 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 61 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 62 | "no-constant-condition": 2, // 禁止在条件中使用常量表达式 if(true) if(1) 63 | "no-trailing-spaces": 1, // 一行结束后面不要有空格 64 | "no-var": 2, // 禁用var,用let和const代替 65 | "consistent-this": [2, "that"], // this别名 66 | indent: ["error", 4], 67 | "no-dupe-args": [2], 68 | // 文件的最大行数 69 | "max-lines": [ 70 | "error", 71 | { 72 | max: 600, 73 | skipBlankLines: true, // 忽略空白行 74 | skipComments: true // 忽略只包含注释的行 75 | } 76 | ], 77 | // 遇见对象花括号换行 78 | "object-curly-newline": [ 79 | "error", 80 | { 81 | ObjectExpression: "always", 82 | ObjectPattern: { 83 | multiline: true 84 | }, 85 | ImportDeclaration: "never", 86 | ExportDeclaration: { 87 | multiline: true, 88 | minProperties: 3 89 | } 90 | } 91 | ] 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/views/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 69 | 70 | 101 | -------------------------------------------------------------------------------- /src/components/TextureEditor/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 92 | 93 | -------------------------------------------------------------------------------- /src/components/center/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 102 | 118 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const EffectScope: typeof import('vue')['EffectScope'] 9 | const computed: typeof import('vue')['computed'] 10 | const createApp: typeof import('vue')['createApp'] 11 | const customRef: typeof import('vue')['customRef'] 12 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 13 | const defineComponent: typeof import('vue')['defineComponent'] 14 | const effectScope: typeof import('vue')['effectScope'] 15 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 16 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 17 | const h: typeof import('vue')['h'] 18 | const inject: typeof import('vue')['inject'] 19 | const isProxy: typeof import('vue')['isProxy'] 20 | const isReactive: typeof import('vue')['isReactive'] 21 | const isReadonly: typeof import('vue')['isReadonly'] 22 | const isRef: typeof import('vue')['isRef'] 23 | const markRaw: typeof import('vue')['markRaw'] 24 | const nextTick: typeof import('vue')['nextTick'] 25 | const onActivated: typeof import('vue')['onActivated'] 26 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 27 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 28 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 29 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 30 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 31 | const onDeactivated: typeof import('vue')['onDeactivated'] 32 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 33 | const onMounted: typeof import('vue')['onMounted'] 34 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 35 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 36 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 37 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 38 | const onUnmounted: typeof import('vue')['onUnmounted'] 39 | const onUpdated: typeof import('vue')['onUpdated'] 40 | const provide: typeof import('vue')['provide'] 41 | const reactive: typeof import('vue')['reactive'] 42 | const readonly: typeof import('vue')['readonly'] 43 | const ref: typeof import('vue')['ref'] 44 | const resolveComponent: typeof import('vue')['resolveComponent'] 45 | const shallowReactive: typeof import('vue')['shallowReactive'] 46 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 47 | const shallowRef: typeof import('vue')['shallowRef'] 48 | const toRaw: typeof import('vue')['toRaw'] 49 | const toRef: typeof import('vue')['toRef'] 50 | const toRefs: typeof import('vue')['toRefs'] 51 | const toValue: typeof import('vue')['toValue'] 52 | const triggerRef: typeof import('vue')['triggerRef'] 53 | const unref: typeof import('vue')['unref'] 54 | const useAttrs: typeof import('vue')['useAttrs'] 55 | const useCssModule: typeof import('vue')['useCssModule'] 56 | const useCssVars: typeof import('vue')['useCssVars'] 57 | const useLink: typeof import('vue-router')['useLink'] 58 | const useRoute: typeof import('vue-router')['useRoute'] 59 | const useRouter: typeof import('vue-router')['useRouter'] 60 | const useSlots: typeof import('vue')['useSlots'] 61 | const watch: typeof import('vue')['watch'] 62 | const watchEffect: typeof import('vue')['watchEffect'] 63 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 64 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 65 | } 66 | // for type re-export 67 | declare global { 68 | // @ts-ignore 69 | export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' 70 | } 71 | -------------------------------------------------------------------------------- /src/assets/styles/pickColor.css: -------------------------------------------------------------------------------- 1 | .zs-color-picker[data-v-87d63fa8] { 2 | position: relative; 3 | text-align: left; 4 | font-size: 14px; 5 | display: inline-block; 6 | outline: none; 7 | } 8 | .zs-color-picker-panel[data-v-87d63fa8] { 9 | position: absolute; 10 | width: 190px; 11 | background: #fff; 12 | border: 1px solid #ddd; 13 | visibility: hidden; 14 | border-radius: 2px; 15 | margin-top: 2px; 16 | padding: 10px 10px 5px; 17 | box-shadow: 0 0 5px #00000026; 18 | opacity: 0; 19 | transition: all 0.3s ease; 20 | box-sizing: content-box; 21 | } 22 | .zs-color-picker-panel__visible[data-v-87d63fa8] { 23 | visibility: visible; 24 | opacity: 1; 25 | z-index: 1; 26 | } 27 | .zs-color-picker-panel h3[data-v-87d63fa8] { 28 | margin: 10px 0 5px; 29 | font-size: 14px; 30 | font-weight: 400; 31 | line-height: 1; 32 | color: #333; 33 | } 34 | .zs-color-picker-panel .color-input[data-v-87d63fa8] { 35 | visibility: hidden; 36 | position: absolute; 37 | left: 0; 38 | bottom: 0; 39 | } 40 | .zs-color-picker-btn[data-v-87d63fa8] { 41 | width: 24px; 42 | height: 24px; 43 | cursor: pointer; 44 | border: 1px solid #eee; 45 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAWElEQVRIiWM8fubkfwYygKWJOSM5+mCAhRLNoxaPWjxq8ajFoxbTyeL/DAfJ0Xjs3Cl7Siwmu4Yht1aDgZEYx6MWj1o8avGoxaMWD3qLya5X//4nqx6HAQC7RBGFzolqTAAAAABJRU5ErkJggg==); 46 | background-size: 10px 10px; 47 | } 48 | .zs-color-picker-btn .zs-color-picker-btn-color[data-v-87d63fa8] { 49 | box-sizing: border-box; 50 | border: 4px solid #fff; 51 | width: 100%; 52 | height: 100%; 53 | } 54 | .panel-header[data-v-87d63fa8] { 55 | overflow: hidden; 56 | line-height: 29px; 57 | } 58 | .color-view-bg[data-v-87d63fa8] { 59 | width: 100px; 60 | height: 30px; 61 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAWElEQVRIiWM8fubkfwYygKWJOSM5+mCAhRLNoxaPWjxq8ajFoxbTyeL/DAfJ0Xjs3Cl7Siwmu4Yht1aDgZEYx6MWj1o8avGoxaMWD3qLya5X//4nqx6HAQC7RBGFzolqTAAAAABJRU5ErkJggg==); 62 | background-size: 10px 10px; 63 | float: left; 64 | } 65 | .color-view[data-v-87d63fa8] { 66 | width: 100px; 67 | height: 30px; 68 | transition: background-color 0.3s ease; 69 | } 70 | .default-color[data-v-87d63fa8] { 71 | width: 80px; 72 | float: right; 73 | text-align: center; 74 | border: 1px solid #ddd; 75 | cursor: pointer; 76 | color: #333; 77 | } 78 | .panel-main .theme-color li[data-v-87d63fa8] { 79 | width: 15px; 80 | height: 15px; 81 | display: inline-block; 82 | margin: 0 2px; 83 | transition: all 0.3s ease; 84 | cursor: pointer; 85 | } 86 | .panel-main .theme-color li[data-v-87d63fa8]:hover { 87 | box-shadow: 0 0 5px #0006; 88 | transform: scale(1.3); 89 | } 90 | .standard-color li[data-v-87d63fa8] { 91 | width: 15px; 92 | display: inline-block; 93 | margin: 0 2px; 94 | } 95 | .standard-color li li[data-v-87d63fa8] { 96 | display: block; 97 | width: 15px; 98 | height: 15px; 99 | transition: all 0.3s ease; 100 | margin: 0; 101 | } 102 | .standard-color li li[data-v-87d63fa8]:hover { 103 | box-shadow: 0 0 5px #0006; 104 | transform: scale(1.3); 105 | } 106 | ul[data-v-87d63fa8], 107 | li[data-v-87d63fa8], 108 | ol[data-v-87d63fa8] { 109 | list-style: none; 110 | margin: 0; 111 | padding: 0; 112 | } 113 | .bottom-btn[data-v-87d63fa8] { 114 | display: flex; 115 | align-items: center; 116 | flex-direction: row; 117 | } 118 | .bottom-btn .finsh[data-v-87d63fa8] { 119 | margin-left: auto; 120 | background: rgb(0, 162, 255); 121 | color: #fff; 122 | cursor: pointer; 123 | margin-right: 5px; 124 | padding: 5px 10px; 125 | border-radius: 2px; 126 | } 127 | -------------------------------------------------------------------------------- /src/stores/three.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref, toRaw } from "vue"; 2 | import * as THREE from "three"; 3 | import T from "@/utils/threeUtils"; 4 | import { defineStore } from "pinia"; 5 | import gsap from "gsap"; 6 | 7 | import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js"; 8 | import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js"; 9 | import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js"; 10 | 11 | export default defineStore("three", () => { 12 | let camera: THREE.PerspectiveCamera = T.initCamera(); 13 | let renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer(); 14 | let scene: THREE.Scene = new THREE.Scene(); 15 | let model: Ref = ref({} as THREE.Object3D); 16 | let composer = {} as EffectComposer; 17 | let outlinePass = ref({} as OutlinePass); 18 | 19 | const setOutLineEffect = (width, height) => { 20 | composer = new EffectComposer(renderer); 21 | const renderPass = new RenderPass(scene, camera); 22 | composer.addPass(renderPass); 23 | outlinePass.value = new OutlinePass( 24 | new THREE.Vector2(width, height), 25 | scene, 26 | camera 27 | ); 28 | 29 | let params = { 30 | edgeStrength: 10, // 边缘强度 31 | edgeGlow: 1, // 边缘发光 32 | edgeThickness: 4, // 边缘厚度 33 | pulsePeriod: 5, // 脉冲周期 34 | rotate: false, 35 | usePatternTexture: false 36 | }; 37 | 38 | outlinePass.value.edgeStrength = Number(params.edgeStrength); 39 | outlinePass.value.edgeGlow = Number(params.edgeGlow); 40 | outlinePass.value.edgeThickness = Number(params.edgeThickness); 41 | outlinePass.value.pulsePeriod = Number(params.pulsePeriod); 42 | outlinePass.value.visibleEdgeColor.set("#ffffff"); 43 | outlinePass.value.hiddenEdgeColor.set("#ffffff"); 44 | 45 | composer.addPass(toRaw(outlinePass.value)); 46 | }; 47 | 48 | /** 49 | * 设置屏幕大小 50 | */ 51 | 52 | const setScreenSize = dom => { 53 | let { width, height } = dom.getBoundingClientRect(); 54 | renderer.setSize(width, height); 55 | camera.aspect = width / height; 56 | camera.updateProjectionMatrix(); 57 | 58 | return { width, height }; 59 | }; 60 | 61 | /** 62 | * 初始化场景 63 | */ 64 | 65 | const initScreen = dom => { 66 | if (renderer && camera && scene) { 67 | let { width, height } = setScreenSize(dom); 68 | setCameraToModelSide(); 69 | setOutLineEffect(width, height); 70 | 71 | let controls = T.addOrbitControls(camera, renderer.domElement); 72 | T.appendCanvasToElement(dom, renderer.domElement); 73 | // controls.enableDamping = true; 74 | 75 | window.addEventListener("resize", () => { 76 | setScreenSize(dom); 77 | }); 78 | 79 | let animate = () => { 80 | requestAnimationFrame(animate); 81 | composer.render(); 82 | 83 | renderer.setClearColor(0x272822); 84 | // renderer!.render(scene!, camera!); 85 | controls.update(); 86 | }; 87 | 88 | animate(); 89 | } 90 | }; 91 | 92 | const setCameraToModelSide = () => { 93 | var box = new THREE.Box3().setFromObject(model.value); 94 | var size = box.getSize(new THREE.Vector3()); 95 | 96 | const helper = new THREE.Box3Helper(box); 97 | scene.add(helper); 98 | 99 | let maxValue = Math.max(size.x, size.y, size.z); 100 | 101 | gsap.to(camera.position, { 102 | x: maxValue * 2.5, 103 | y: maxValue * 2.5, 104 | z: maxValue * 2.5, 105 | onUpdate: function () { 106 | camera.lookAt(model.value.position); 107 | }, 108 | onComplete: function () { 109 | console.log(camera.position); 110 | } 111 | }); 112 | }; 113 | 114 | return { 115 | camera, 116 | renderer, 117 | scene, 118 | model, 119 | initScreen, 120 | composer, 121 | outlinePass 122 | }; 123 | }); 124 | -------------------------------------------------------------------------------- /src/utils/threeUtils.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { PointerLockControls } from "three/examples/jsm/controls/PointerLockControls"; 3 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; 4 | import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader"; 5 | import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader"; 6 | import { DDSLoader } from "three/examples/jsm/loaders/DDSLoader"; 7 | import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; 8 | import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; 9 | const sizeConfig = { 10 | height: window.innerHeight, 11 | width: window.innerWidth 12 | }; 13 | 14 | /** 15 | * 添加生成的canvas 16 | * @param element 17 | * @param canvas 18 | */ 19 | 20 | const appendCanvasToElement = ( 21 | element: HTMLElement, 22 | canvas: HTMLCanvasElement 23 | ) => { 24 | element.appendChild(canvas); 25 | }; 26 | 27 | /** 28 | * 添加控制器 29 | * @param camera 30 | * @param canvas 31 | * @returns 32 | */ 33 | 34 | const addOrbitControls = (camera: THREE.Camera, canvas: HTMLCanvasElement) => { 35 | const controls = new OrbitControls(camera, canvas); 36 | 37 | return controls; 38 | }; 39 | 40 | /** 41 | * 添加控制器 42 | * @param camera 43 | * @param canvas 44 | * @returns 45 | */ 46 | 47 | const addLockControls = (camera: THREE.Camera) => { 48 | const controls = new PointerLockControls(camera, document.body); 49 | 50 | return controls; 51 | }; 52 | 53 | /** 54 | * 添加照相机 55 | * @param type 56 | * @param near 57 | * @param far 58 | * @returns 59 | */ 60 | const initCamera = (type = "PerspectiveCamera", near = 0.1, far = 1000) => { 61 | let camera; 62 | if (type === "PerspectiveCamera") { 63 | camera = new THREE.PerspectiveCamera( 64 | 45, 65 | sizeConfig.width / sizeConfig.height, 66 | near, 67 | far 68 | ); 69 | } else { 70 | const k = sizeConfig.width / sizeConfig.height; //窗口宽高比 71 | const s = 150; 72 | //创建相机对象 73 | camera = new THREE.OrthographicCamera( 74 | -150 * k, 75 | 150 * k, 76 | 150, 77 | -150, 78 | near, 79 | far 80 | ); 81 | } 82 | 83 | return camera; 84 | }; 85 | 86 | /** 87 | * 添加渲染器 88 | * @param params 89 | * @returns 90 | */ 91 | 92 | const initRenderer = ( 93 | size?: { width: number; height: number }, 94 | params: object = {} 95 | ): THREE.WebGLRenderer => { 96 | const renderer = new THREE.WebGLRenderer({ 97 | ...params 98 | }); 99 | renderer.shadowMap.enabled = true; 100 | 101 | console.log(size); 102 | renderer.setSize( 103 | size?.width || sizeConfig.width, 104 | size?.height || sizeConfig.height 105 | ); 106 | 107 | return renderer; 108 | }; 109 | 110 | /** 111 | * 添加BoxGeometry 112 | * @param sizeConfig 113 | * @param materialType 114 | * @param materialConfig 115 | * @returns 116 | */ 117 | 118 | const generateCube = ( 119 | sizeConfig: number[] = [100, 100, 100], 120 | materialType = "MeshBasicMaterial", 121 | materialConfig: {} 122 | ) => { 123 | return new THREE.Mesh( 124 | new THREE.BoxGeometry(sizeConfig[0], sizeConfig[1], sizeConfig[2]), 125 | new (THREE as any)[materialType]({ ...materialConfig }) 126 | ); 127 | }; 128 | 129 | /** 130 | * aspect scene 131 | */ 132 | 133 | const addAdaptionScreen = ( 134 | renderer: THREE.Renderer, 135 | camera: THREE.PerspectiveCamera | THREE.OrthographicCamera, 136 | size 137 | ) => { 138 | window.addEventListener("resize", () => { 139 | sizeConfig.height = window.innerHeight; 140 | sizeConfig.width = window.innerWidth; 141 | camera.updateProjectionMatrix(); 142 | 143 | if (camera instanceof THREE.PerspectiveCamera) { 144 | camera.aspect = sizeConfig.width / sizeConfig.height; 145 | } 146 | 147 | renderer.setSize(sizeConfig.width, sizeConfig.height); 148 | }); 149 | }; 150 | 151 | /** 152 | * 判断射线是否接触到物体 153 | * @param raycaster 154 | * @param objectList 155 | * @returns 156 | */ 157 | 158 | const judgeRaycasterTouchObject = ( 159 | raycaster: THREE.Raycaster, 160 | objectList: THREE.Object3D[], 161 | pointer: THREE.Vector2, 162 | camera: THREE.Camera 163 | ) => { 164 | raycaster.setFromCamera(pointer, camera); 165 | const intersectObject = raycaster.intersectObjects(objectList, false); 166 | return { isTouch: Boolean(intersectObject.length), intersectObject }; 167 | }; 168 | 169 | /** 170 | * 添加贴图加载器 171 | * @param textureUrl 172 | * @param callback 173 | */ 174 | const addTextureLoader = ( 175 | textureUrl: string, 176 | callback: (arg1: object) => any 177 | ) => { 178 | const fileExtension = textureUrl.split(".").pop(); 179 | const isDDS = fileExtension === "dds"; 180 | 181 | let loader; 182 | 183 | if (isDDS) { 184 | loader = new DDSLoader(); 185 | } else { 186 | loader = new THREE.TextureLoader(); 187 | } 188 | 189 | loader.load(textureUrl, object => { 190 | callback && callback(object); 191 | }); 192 | }; 193 | 194 | /** 195 | * 添加位置相关的音频对象 196 | * @param audioUrl 197 | * @param camera 198 | * @param volume 199 | * @param refDistance 200 | */ 201 | 202 | const addPositionalAudio = ( 203 | audioUrl: string, 204 | camera: THREE.Camera, 205 | volume: number, 206 | refDistance: number 207 | ) => { 208 | return new Promise((resolve, reject) => { 209 | const listener = new THREE.AudioListener(); 210 | camera.add(listener); 211 | const PositionalAudio = new THREE.PositionalAudio(listener); 212 | 213 | const audioLoader = new THREE.AudioLoader(); 214 | 215 | audioLoader.load(audioUrl, AudioBuffer => { 216 | PositionalAudio.setBuffer(AudioBuffer); 217 | PositionalAudio.setVolume(volume || 0.9); //音量 218 | PositionalAudio.setRefDistance(refDistance || 1); //参数值越大,声音越大 219 | 220 | resolve(PositionalAudio); 221 | }); 222 | }); 223 | }; 224 | 225 | /** 226 | * 生成地板 227 | */ 228 | 229 | const addPlane = (size: number, params: object): THREE.Mesh => { 230 | const plane = new THREE.Mesh( 231 | new THREE.PlaneGeometry(size, size), 232 | new THREE.MeshBasicMaterial({ 233 | ...params 234 | }) 235 | ); 236 | 237 | plane.rotation.x = -Math.PI / 2; 238 | 239 | return plane; 240 | }; 241 | 242 | /** 243 | * 加载fbx 244 | */ 245 | 246 | const loadFBX = (url: string, callback: (arg0: object) => void) => { 247 | const loader = new FBXLoader(); 248 | // 加载人物 249 | loader.load(url, object => { 250 | callback && callback(object); 251 | }); 252 | }; 253 | 254 | /** 255 | * 加载OBJ 256 | */ 257 | 258 | const loadOBJ = (url: string, callback: (arg0: object) => void) => { 259 | const loader = new OBJLoader(); 260 | // 加载人物 261 | loader.load(url, object => { 262 | callback && callback(object); 263 | }); 264 | }; 265 | 266 | /** 267 | * 加载GLTF 268 | */ 269 | 270 | const loadGLTF = async (url: string) => { 271 | return new Promise(resolve => { 272 | const loader = new GLTFLoader(); 273 | const dracoLoader = new DRACOLoader() 274 | dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/'); 275 | loader.setDRACOLoader(dracoLoader) 276 | loader.load(url, object => { 277 | resolve(object as unknown as THREE.Group); 278 | }); 279 | }); 280 | }; 281 | 282 | const calcObject3DSize = (object3d: THREE.Object3D) => { 283 | const box = new THREE.Box3().setFromObject(object3d); 284 | const width = box.max.x + box.min.x; 285 | const height = box.max.y + box.min.y; 286 | 287 | return { width, height }; 288 | }; 289 | 290 | export default { 291 | addPlane, 292 | appendCanvasToElement, 293 | generateCube, 294 | addOrbitControls, 295 | initCamera, 296 | initRenderer, 297 | addAdaptionScreen, 298 | judgeRaycasterTouchObject, 299 | addTextureLoader, 300 | addPositionalAudio, 301 | addLockControls, 302 | loadFBX, 303 | loadOBJ, 304 | loadGLTF, 305 | calcObject3DSize 306 | }; 307 | -------------------------------------------------------------------------------- /src/components/right/index.vue: -------------------------------------------------------------------------------- 1 | 125 | 126 | 301 | 346 | --------------------------------------------------------------------------------