├── 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 |
2 |
5 |
6 |
7 |
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 |
2 |
3 |
4 |
5 | {{ title }}
6 |
7 |
8 |
9 |
10 |
11 |
44 |
45 |
--------------------------------------------------------------------------------
/src/components/ProNumberInput/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
69 |
70 |
101 |
--------------------------------------------------------------------------------
/src/components/TextureEditor/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
92 |
93 |
--------------------------------------------------------------------------------
/src/components/center/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | 点击或者拖拽进行上传
12 | 请上传 GLTF 或 GLB 文件
13 |
14 |
15 |
16 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | 类型
8 | {{ state.type }}
9 |
10 |
14 |
15 |
16 | 顶点数
17 | {{ state.vertexCount }}
18 |
19 |
20 | 三角数
21 | {{ state.triangleCount }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
位置
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
旋转
39 |
41 |
42 |
44 |
45 |
47 |
48 |
49 |
50 |
缩放
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 颜色
63 |
64 |
65 |
69 |
73 |
74 | 贴图
75 |
76 |
77 |
81 |
82 | 自发光贴图
83 |
84 |
85 |
86 | 透明贴图
87 |
88 |
89 |
90 | 凹凸贴图
91 |
92 |
93 |
94 | 法线贴图
95 |
96 |
97 |
98 | 位移贴图
99 |
100 |
101 |
102 | 粗糙贴图
103 |
104 |
105 |
106 | 金属贴图
107 |
108 |
109 |
113 |
117 |
121 |
122 |
123 |
124 |
125 |
126 |
301 |
346 |
--------------------------------------------------------------------------------