diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 000000000..f811ebde5 --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,40 @@ +name: github pages + +on: + push: + branches: + - main + +env: + GITEA_MAIL: ${{ secrets.GITEA_MAIL }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + +jobs: + build-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + ref: main + submodules: true + fetch-depth: 0 + - run: | + yarn install + rm -rf dist/vrma + git clone https://${GITEA_TOKEN}@git.syui.ai/ai/vrma dist/vrma + + - name: Build + env: + TZ: "Asia/Tokyo" + run: | + yarn build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist + user_name: 'ai[bot]' + user_email: '138105980+yui-syui-ai[bot]@users.noreply.github.com' diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..d3e6b1306 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +example +yarn.lock +**DS_Store +dist/*.js diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..bf34da5a2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dist/vrma"] + path = dist/vrma + url = git@git.syui.ai:ai/vrma diff --git a/README.md b/README.md index ccdc4c76d..dedffe126 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,30 @@ +# vrm + +three-vrm + vrma + +## vrm & vrma ```sh -$ npm i -$ npm run dev -$ npm run build +$ git submodule update --init --recursive ``` + +or + +- vrm : [download](https://hub.vroid.com/characters/675572020956181239/models/7175071267176594918) +- vrma : [download](https://vroid.booth.pm/items/5512385) + +> ./dist/vrma + +```js +load("/vrma/model.vrm"); +load("/vrma/VRMA_01.vrma"); +``` + +## build + +```sh +$ yarn install +$ yarn dev +$ yarn build +``` + diff --git a/dist/CNAME b/dist/CNAME new file mode 100644 index 000000000..63a8a5072 --- /dev/null +++ b/dist/CNAME @@ -0,0 +1 @@ +vrm.syui.ai diff --git a/dist/css/icomoon/css/icomoon.css b/dist/css/icomoon/css/icomoon.css new file mode 100644 index 000000000..0d3359f8f --- /dev/null +++ b/dist/css/icomoon/css/icomoon.css @@ -0,0 +1,72 @@ +@font-face { + font-family: 'icomoon'; + src: url('../fonts/icomoon.eot?czi5du'); + src: url('../fonts/icomoon.eot?czi5du#iefix') format('embedded-opentype'), + url('../fonts/icomoon.ttf?czi5du') format('truetype'), + url('../fonts/icomoon.woff?czi5du') format('woff'), + url('../fonts/icomoon.svg?czi5du#icomoon') format('svg'); + font-weight: normal; + font-style: normal; + font-display: block; +} + +[class^="icon-"], [class*=" icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'icomoon' !important; + speak: never; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-ai:before { + content: "\e908"; +} +.icon-robot:before { + content: "\e909"; +} +.icon-api:before { + content: "\e90e"; +} +.icon-sandar:before { + content: "\e90a"; +} +.icon-moon:before { + content: "\e90b"; +} +.icon-home:before { + content: "\e90c"; +} +.icon-cloud:before { + content: "\e90d"; +} +.icon-phoenix:before { + content: "\e906"; +} +.icon-honeycomb:before { + content: "\e907"; +} +.icon-phoenix-power:before { + content: "\e903"; +} +.icon-phoenix-world:before { + content: "\e904"; +} +.icon-power:before { + content: "\e905"; +} +.icon-syui:before { + content: "\e902"; +} +.icon-archlinux:before { + content: "\e900"; +} +.icon-archlinuxjp:before { + content: "\e901"; +} diff --git a/dist/css/icomoon/fonts/icomoon.eot b/dist/css/icomoon/fonts/icomoon.eot new file mode 100644 index 000000000..d120dd487 Binary files /dev/null and b/dist/css/icomoon/fonts/icomoon.eot differ diff --git a/dist/css/icomoon/fonts/icomoon.svg b/dist/css/icomoon/fonts/icomoon.svg new file mode 100644 index 000000000..25356f9bc --- /dev/null +++ b/dist/css/icomoon/fonts/icomoon.svg @@ -0,0 +1,25 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dist/css/icomoon/fonts/icomoon.ttf b/dist/css/icomoon/fonts/icomoon.ttf new file mode 100644 index 000000000..f366cf679 Binary files /dev/null and b/dist/css/icomoon/fonts/icomoon.ttf differ diff --git a/dist/css/icomoon/fonts/icomoon.woff b/dist/css/icomoon/fonts/icomoon.woff new file mode 100644 index 000000000..13ec0c2dd Binary files /dev/null and b/dist/css/icomoon/fonts/icomoon.woff differ diff --git a/dist/css/style.css b/dist/css/style.css new file mode 100644 index 000000000..4331c61c9 --- /dev/null +++ b/dist/css/style.css @@ -0,0 +1,36 @@ +div#canvas { + width: 100%; + height: 640px; +} + +div#menu { + padding: 20px; + border-bottom:solid 1px #ccc; +} + +button { + border: none; + margin: 0; + padding: 0 20px 0 20px; + width: auto; + overflow: visible; + background: transparent; + color: inherit; + font: inherit; + line-height: normal; + -webkit-font-smoothing: inherit; + -moz-osx-font-smoothing: inherit; + appearance: none; + -webkit-appearance: none; + cursor: pointer; + user-select: none; +} + +a { +text-decoration: none; +} + +a:hover{ + /* color: #fff700; */ + color: #847e00; +} diff --git a/dist/favicon.ico b/dist/favicon.ico new file mode 100644 index 000000000..7f94540e2 Binary files /dev/null and b/dist/favicon.ico differ diff --git a/dist/img/0.hdr b/dist/img/0.hdr new file mode 100644 index 000000000..74bef49a8 Binary files /dev/null and b/dist/img/0.hdr differ diff --git a/dist/img/1.hdr b/dist/img/1.hdr new file mode 100644 index 000000000..bc15b9a0c Binary files /dev/null and b/dist/img/1.hdr differ diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 000000000..99e68ca8b --- /dev/null +++ b/dist/index.html @@ -0,0 +1,18 @@ + + + + ai + + + + + + + +
+ + diff --git a/dist/vrma b/dist/vrma new file mode 160000 index 000000000..3e83eb3ef --- /dev/null +++ b/dist/vrma @@ -0,0 +1 @@ +Subproject commit 3e83eb3efde28bcc4665c63ad6f0c02b849619e3 diff --git a/package.json b/package.json new file mode 100644 index 000000000..8428f1f84 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "vrm", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "webpack", + "dev": "webpack-dev-server --open" + }, + "devDependencies": { + "ts-loader": "^9.5.1", + "typescript": "^5.4.2", + "webpack": "^5.90.3", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.3" + }, + "dependencies": { + "@pixiv/three-vrm": "^2.1.1", + "@pixiv/three-vrm-animation": "^2.1.1", + "@pixiv/three-vrm-springbone": "^2.1.1", + "three": "^0.162.0" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..eb73b0ecb --- /dev/null +++ b/src/index.ts @@ -0,0 +1,266 @@ +import * as THREE from "three" +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader" +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; +import { VRMLoaderPlugin } from "@pixiv/three-vrm"; +import { createVRMAnimationClip, VRMAnimationLoaderPlugin } from "@pixiv/three-vrm-animation"; + +import { Color, DirectionalLight, Fog, HemisphereLight } from 'three'; +import { GridHelper, Mesh, MeshLambertMaterial, BoxGeometry, Vector3 } from 'three'; + +import { VRMSpringBoneManager, VRMSpringBoneJoint, VRMSpringBoneJointHelper } from '@pixiv/three-vrm-springbone'; + +import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader"; + +window.addEventListener("DOMContentLoaded", () => { + const canvas = document.getElementById("canvas"); + if (canvas == null) return; + + const scene = new THREE.Scene(); + + const camera = new THREE.PerspectiveCamera( + 30, canvas.clientWidth/canvas.clientHeight, 0.1, 20); + camera.position.set(0.0, 0.9, -4.0) + camera.rotation.set(0.0, Math.PI, 0.0) + camera.lookAt(new THREE.Vector3(0, 0, 0)); + + // https://threejs.org/docs/#api/en/constants/Renderer + const renderer = new THREE.WebGLRenderer({antialias: true, alpha: true}); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(canvas.clientWidth, canvas.clientHeight); + renderer.setClearColor(0x7fbfff, 1.0); + renderer.shadowMap.enabled = true; + renderer.outputColorSpace = THREE.SRGBColorSpace; + renderer.toneMapping = THREE.ReinhardToneMapping; + renderer.toneMapping = THREE.NeutralToneMapping; + canvas.appendChild(renderer.domElement); + renderer.toneMappingExposure = 1.5; + + //renderer.toneMapping = THREE.ACESFilmicToneMapping; + + const light = new THREE.DirectionalLight(0xffffff, Math.PI); + light.position.set(1.0, 1.0, 1.0); + scene.add(light); + + let currentVrm: any = undefined; + let currentVrmAnimation: any = undefined; + let currentMixer:any = undefined; + + function load(url: string) { + loader.load( + url, + (gltf) => { + tryInitVRM(gltf); + tryInitVRMA(gltf); + }, + (progress) => console.log( + "Loading model...", + 100.0 * (progress.loaded / progress.total), "%" + ), + (error) => console.error(error) + ); + } + + function tryInitVRM(gltf: any) { + const vrm = gltf.userData.vrm; + if ( vrm == null ) { + return; + } + currentVrm = vrm; + scene.add(vrm.scene); + initAnimationClip(); + } + + function tryInitVRMA(gltf: any) { + const vrmAnimations = gltf.userData.vrmAnimations; + if (vrmAnimations == null) { + return; + } + currentVrmAnimation = vrmAnimations[0] ?? null; + initAnimationClip(); + } + + function initAnimationClip() { + if (currentVrm && currentVrmAnimation) { + currentMixer = new THREE.AnimationMixer(currentVrm.scene); + const clip = createVRMAnimationClip(currentVrmAnimation, currentVrm); + currentMixer.clipAction(clip).play(); + } + } + + const loader = new GLTFLoader(); + loader.register((parser) => { + return new VRMLoaderPlugin(parser); + }); + loader.register((parser) => { + return new VRMAnimationLoaderPlugin(parser); + }); + + load("/vrma/ai.vrm"); + load("/vrma/fly_c.vrma"); + + const clock = new THREE.Clock(); + clock.start(); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.2; + controls.enableRotate = true; + controls.target.set( 0.0, 1.0, 0.0 ); + + function floor_default(){ + scene.background = new THREE.Color( 0xffffff ); + const directionalLight = new THREE.DirectionalLight(0xffffff); + directionalLight.position.set(1, 1, 1); + scene.add(directionalLight); + const ambientLight = new THREE.AmbientLight(0x333333); + scene.add(ambientLight); + + const floor = new Mesh( + new BoxGeometry(50, 100), + new MeshLambertMaterial({ + color: 0xffffff, + depthWrite: true, + }) + ); + floor.position.y = -1.0; + floor.rotation.x = -Math.PI / 2; + scene.add(floor); + + const grid = new GridHelper(50, 100, 0xffffff, 0xffffff); + scene.add(grid); + grid.position.set(Math.round(0), 0, Math.round(0)); + scene.fog = new Fog(0xffffff, 3, 20); + scene.fog?.color.set(0xffffff); + } + + scene.background = new THREE.Color( 0xffffff ); + const directionalLight = new THREE.DirectionalLight(0xffffff); + directionalLight.position.set(1, 1, 1); + scene.add(directionalLight); + const ambientLight = new THREE.AmbientLight(0x333333); + scene.add(ambientLight); + + //const floor = new Mesh( + // new BoxGeometry(50, 100), + // new MeshLambertMaterial({ + // color: 0xffffff, + // depthWrite: true, + // }) + //); + //floor.position.y = -1.0; + //floor.rotation.x = -Math.PI / 2; + //scene.add(floor); + + const grid = new GridHelper(50, 100, 0xffffff, 0xffffff); + scene.add(grid); + grid.position.set(Math.round(0), 0, Math.round(0)); + scene.fog = new Fog(0xffffff, 3, 20); + scene.fog?.color.set(0xffffff); + + function animate() { + controls.update(); + const delta = clock.getDelta(); + if (currentMixer) { + currentMixer.update(delta); + } + if (currentVrm) { + currentVrm.update(delta); + } + requestAnimationFrame(animate); + scene.rotation.y += 0.005; + renderer.render(scene, camera); + } + animate(); + + function random_happy() { + // https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0/expressions.ja.md + currentVrm.expressionManager.setValue('relaxed', 0.5); + } + + function random_head() { + // https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0/lookAt.ja.md + currentVrm.lookAt.target = camera; + currentVrm.VRMLookAtBoneApplier = camera; + currentVrm.VRMLookAtExpressionApplier = camera; + + // https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0/humanoid.ja.md + const head = currentVrm.humanoid.getRawBoneNode("head"); + head.target = camera; + } + + function random_blink(){ + setInterval(() => { + currentVrm.expressionManager.setValue('relaxed', 0); + currentVrm.expressionManager.setValue('blink', 0); + random_head(); + const r = Math.floor(Math.random() * 3); + if (r == 1) { + setTimeout(() => { currentVrm.expressionManager.setValue('blink', 1); }, 5000); + setTimeout(() => { + currentVrm.expressionManager.setValue('blink', 0); + }, 5500); + }; + setTimeout(() => { + currentVrm.expressionManager.setValue('relaxed', 0.5); + currentVrm.expressionManager.setValue('blink', 1); + }, 6000); + }, 6500); + } + random_blink(); + + setInterval(() => { + const r = Math.floor(Math.random() * 4 + 1); + load("/vrma/" + r + ".vrma"); + setTimeout(() => { + load("/vrma/fly_c.vrma"); + }, 10000); + }, 15000); + + const el_light = document.querySelector('#btn-moon') as HTMLInputElement | null; + if(el_light != null) { + el_light.addEventListener('click', function(){ + light_s(); + }); + } + + let light_enable = true; + function light_s(){ + if (light_enable == true) { + light_enable = false; + renderer.toneMapping = THREE.ACESFilmicToneMapping; + light.intensity = -0.5; + scene.background = new THREE.Color(0x000000); + scene.fog = new Fog(0x000000, 3, 20); + } else { + light_enable = true; + renderer.toneMapping = THREE.NeutralToneMapping; + light.intensity = 1; + scene.background = new THREE.Color(0xffffff); + scene.fog = new Fog(0xffffff, 3, 20); + } + console.log(light_enable); + } + + const el_hdr = document.querySelector('#btn-sandar') as HTMLInputElement | null; + if(el_hdr != null) { + el_hdr.addEventListener('click', function(){ + hdr_s(); + }); + } + + let hdr_r = 0; + function hdr_s() { + if (hdr_r == 0) { + hdr_r = 1; + } else { + hdr_r = 0; + } + let hdr = "/img/" + hdr_r + ".hdr"; + new RGBELoader().load(hdr, function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + scene.background = texture; + scene.environment = texture; + }); + } + +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..6794acd31 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "skipLibCheck": true + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 000000000..8c36b9cf4 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,26 @@ +const path = require('path'); + +module.exports = { + mode: 'development', + entry: './src/index.ts', + module: { + rules: [ + { + test: /\.ts$/, + loader: 'ts-loader' + } + ] + }, + resolve: { + extensions: ['.ts', '.js'] + }, + output: { + filename: 'main.js', + path: path.join(__dirname, "dist") + }, + devServer: { + static: { + directory: path.join(__dirname, "dist"), + } + } +}