Add 3D character rendering to Character3D component
- Integrated Three.js for 3D character visualization based on user gender and age. - Simplified the character structure by removing outdated HTML elements and replacing them with a dynamic 3D model loader. - Implemented model loading with fallback options and added animation capabilities for enhanced visual appeal. - Updated CSS for the character container to ensure proper rendering and responsiveness.
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"mitt": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"three": "^0.169.0",
|
||||
"vue": "~3.4.31",
|
||||
"vue-i18n": "^10.0.0-beta.2",
|
||||
"vue-multiselect": "^3.1.0",
|
||||
|
||||
40
frontend/public/models/3d/falukant/characters/README.md
Normal file
40
frontend/public/models/3d/falukant/characters/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 3D-Charakter-Modelle
|
||||
|
||||
## Verzeichnisstruktur
|
||||
|
||||
Dieses Verzeichnis enthält die 3D-Modelle für Falukant-Charaktere.
|
||||
|
||||
## Dateinamen-Konvention
|
||||
|
||||
### Basis-Modelle (Fallback)
|
||||
- `male.glb` - Basis-Modell männlich
|
||||
- `female.glb` - Basis-Modell weiblich
|
||||
|
||||
### Altersspezifische Modelle
|
||||
- `male_toddler.glb` - Männlich, Kleinkind (0-3 Jahre)
|
||||
- `male_child.glb` - Männlich, Kind (4-7 Jahre)
|
||||
- `male_preteen.glb` - Männlich, Vor-Teenager (8-12 Jahre)
|
||||
- `male_teen.glb` - Männlich, Teenager (13-17 Jahre)
|
||||
- `male_adult.glb` - Männlich, Erwachsen (18+ Jahre)
|
||||
- `female_toddler.glb` - Weiblich, Kleinkind (0-3 Jahre)
|
||||
- `female_child.glb` - Weiblich, Kind (4-7 Jahre)
|
||||
- `female_preteen.glb` - Weiblich, Vor-Teenager (8-12 Jahre)
|
||||
- `female_teen.glb` - Weiblich, Teenager (13-17 Jahre)
|
||||
- `female_adult.glb` - Weiblich, Erwachsen (18+ Jahre)
|
||||
|
||||
## Fallback-Verhalten
|
||||
|
||||
Wenn kein spezifisches Modell für den Altersbereich existiert, wird automatisch das Basis-Modell (`male.glb` / `female.glb`) verwendet.
|
||||
|
||||
## Dateigröße
|
||||
|
||||
- Empfohlen: < 500KB pro Modell
|
||||
- Maximal: 1MB pro Modell
|
||||
|
||||
## Optimierung
|
||||
|
||||
Vor dem Hochladen:
|
||||
1. In Blender öffnen
|
||||
2. Decimate Modifier anwenden (falls nötig)
|
||||
3. Texturen komprimieren (WebP, max 1024x1024)
|
||||
4. GLB Export mit Compression aktiviert
|
||||
BIN
frontend/public/models/3d/falukant/characters/female.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/female.glb
Normal file
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/female_adult.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/female_adult.glb
Normal file
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/female_child.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/female_child.glb
Normal file
Binary file not shown.
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/female_preteen.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/female_preteen.glb
Normal file
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/female_teen.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/female_teen.glb
Normal file
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/female_toddler.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/female_toddler.glb
Normal file
Binary file not shown.
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/male.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/male.glb
Normal file
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/male_adult.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/male_adult.glb
Normal file
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/male_child.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/male_child.glb
Normal file
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/male_preteen.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/male_preteen.glb
Normal file
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/male_teen.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/male_teen.glb
Normal file
Binary file not shown.
BIN
frontend/public/models/3d/falukant/characters/male_toddler.glb
Normal file
BIN
frontend/public/models/3d/falukant/characters/male_toddler.glb
Normal file
Binary file not shown.
@@ -1,31 +1,11 @@
|
||||
<template>
|
||||
<div class="character-3d-container" :class="[`gender-${actualGender}`, `age-${ageGroup}`]">
|
||||
<div class="character-3d" :style="characterStyle">
|
||||
<!-- Kopf -->
|
||||
<div class="head">
|
||||
<div class="face">
|
||||
<div class="eye left"></div>
|
||||
<div class="eye right"></div>
|
||||
<div class="mouth"></div>
|
||||
</div>
|
||||
<!-- Haare -->
|
||||
<div class="hair" :class="actualGender"></div>
|
||||
</div>
|
||||
<!-- Körper -->
|
||||
<div class="body" :class="actualGender">
|
||||
<div class="chest"></div>
|
||||
</div>
|
||||
<!-- Arme -->
|
||||
<div class="arm left"></div>
|
||||
<div class="arm right"></div>
|
||||
<!-- Beine -->
|
||||
<div class="leg left"></div>
|
||||
<div class="leg right"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="container" class="character-3d-container"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
|
||||
export default {
|
||||
name: 'Character3D',
|
||||
props: {
|
||||
@@ -43,7 +23,14 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
randomGender: null,
|
||||
randomAge: null
|
||||
randomAge: null,
|
||||
scene: null,
|
||||
camera: null,
|
||||
renderer: null,
|
||||
model: null,
|
||||
animationId: null,
|
||||
mixer: null,
|
||||
clock: new THREE.Clock()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -71,37 +58,237 @@ export default {
|
||||
ageGroup() {
|
||||
const age = this.actualAge;
|
||||
if (age <= 3) return 'toddler';
|
||||
if (age <= 12) return 'child';
|
||||
if (age <= 7) return 'child';
|
||||
if (age <= 12) return 'preteen';
|
||||
if (age <= 17) return 'teen';
|
||||
if (age <= 30) return 'young-adult';
|
||||
if (age <= 50) return 'adult';
|
||||
if (age <= 70) return 'senior';
|
||||
return 'elderly';
|
||||
return 'adult';
|
||||
},
|
||||
characterStyle() {
|
||||
const age = this.actualAge;
|
||||
let scale = 1;
|
||||
modelPath() {
|
||||
const gender = this.actualGender;
|
||||
const ageGroup = this.ageGroup;
|
||||
|
||||
// Skalierung basierend auf Alter
|
||||
if (age <= 3) {
|
||||
scale = 0.4; // Kleinkind
|
||||
} else if (age <= 12) {
|
||||
scale = 0.6; // Kind
|
||||
} else if (age <= 17) {
|
||||
scale = 0.85; // Teenager
|
||||
} else if (age <= 30) {
|
||||
scale = 1.0; // Junger Erwachsener
|
||||
} else if (age <= 50) {
|
||||
scale = 0.95; // Erwachsener
|
||||
} else if (age <= 70) {
|
||||
scale = 0.9; // Senior
|
||||
} else {
|
||||
scale = 0.85; // Älterer Senior
|
||||
// Versuche zuerst altersspezifisches Modell
|
||||
const specificModel = `/models/3d/falukant/characters/${gender}_${ageGroup}.glb`;
|
||||
|
||||
// Fallback auf Basis-Modell
|
||||
return specificModel;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
actualGender() {
|
||||
this.loadModel();
|
||||
},
|
||||
ageGroup() {
|
||||
this.loadModel();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init3D();
|
||||
this.loadModel();
|
||||
this.animate();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.cleanup();
|
||||
},
|
||||
methods: {
|
||||
init3D() {
|
||||
const container = this.$refs.container;
|
||||
if (!container) return;
|
||||
|
||||
// Scene erstellen
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0xf0f0f0);
|
||||
|
||||
// Camera erstellen
|
||||
const aspect = container.clientWidth / container.clientHeight;
|
||||
this.camera = new THREE.PerspectiveCamera(50, aspect, 0.1, 1000);
|
||||
this.camera.position.set(0, 1.5, 3);
|
||||
this.camera.lookAt(0, 1, 0);
|
||||
|
||||
// Renderer erstellen
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
this.renderer.setSize(container.clientWidth, container.clientHeight);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
container.appendChild(this.renderer.domElement);
|
||||
|
||||
// Licht hinzufügen
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
directionalLight.position.set(5, 10, 5);
|
||||
this.scene.add(directionalLight);
|
||||
|
||||
// Zusätzliches Licht von hinten
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.3);
|
||||
backLight.position.set(-5, 0, -5);
|
||||
this.scene.add(backLight);
|
||||
|
||||
// Resize Handler
|
||||
window.addEventListener('resize', this.onWindowResize);
|
||||
},
|
||||
|
||||
async loadModel() {
|
||||
if (!this.scene) return;
|
||||
|
||||
// Altes Modell entfernen
|
||||
if (this.model) {
|
||||
this.scene.remove(this.model);
|
||||
// Cleanup des alten Modells
|
||||
this.model.traverse((object) => {
|
||||
if (object.geometry) object.geometry.dispose();
|
||||
if (object.material) {
|
||||
if (Array.isArray(object.material)) {
|
||||
object.material.forEach(m => m.dispose());
|
||||
} else {
|
||||
object.material.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.model = null;
|
||||
}
|
||||
|
||||
if (this.mixer) {
|
||||
this.mixer = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const loader = new GLTFLoader();
|
||||
const modelPath = this.modelPath;
|
||||
|
||||
// Versuche zuerst spezifisches Modell, dann Fallback
|
||||
let gltf;
|
||||
try {
|
||||
gltf = await loader.loadAsync(modelPath);
|
||||
} catch (error) {
|
||||
console.warn(`Could not load ${modelPath}, trying fallback model`);
|
||||
// Fallback auf Basis-Modell
|
||||
const fallbackPath = `/models/3d/falukant/characters/${this.actualGender}.glb`;
|
||||
gltf = await loader.loadAsync(fallbackPath);
|
||||
}
|
||||
|
||||
this.model = gltf.scene;
|
||||
|
||||
// Modell zentrieren und skalieren
|
||||
const box = new THREE.Box3().setFromObject(this.model);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
|
||||
// Skalierung basierend auf Alter
|
||||
const age = this.actualAge;
|
||||
let scale = 1;
|
||||
if (age <= 3) {
|
||||
scale = 0.4;
|
||||
} else if (age <= 7) {
|
||||
scale = 0.6;
|
||||
} else if (age <= 12) {
|
||||
scale = 0.75;
|
||||
} else if (age <= 17) {
|
||||
scale = 0.9;
|
||||
} else {
|
||||
scale = 1.0;
|
||||
}
|
||||
|
||||
// Modell skalieren, damit es in die Szene passt
|
||||
const maxDimension = Math.max(size.x, size.y, size.z);
|
||||
const targetHeight = 2; // Zielhöhe in 3D-Einheiten
|
||||
const modelScale = (targetHeight / maxDimension) * scale;
|
||||
this.model.scale.set(modelScale, modelScale, modelScale);
|
||||
|
||||
// Modell zentrieren
|
||||
this.model.position.sub(center.multiplyScalar(modelScale));
|
||||
this.model.position.y = 0; // Auf Boden setzen
|
||||
|
||||
this.scene.add(this.model);
|
||||
|
||||
// Animationen laden falls vorhanden
|
||||
if (gltf.animations && gltf.animations.length > 0) {
|
||||
this.mixer = new THREE.AnimationMixer(this.model);
|
||||
gltf.animations.forEach((clip) => {
|
||||
this.mixer.clipAction(clip).play();
|
||||
});
|
||||
}
|
||||
|
||||
// Sanfte Rotation-Animation
|
||||
if (this.model) {
|
||||
this.model.rotation.y = Math.random() * Math.PI * 2;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading 3D model:', error);
|
||||
}
|
||||
},
|
||||
|
||||
animate() {
|
||||
this.animationId = requestAnimationFrame(this.animate);
|
||||
|
||||
const delta = this.clock.getDelta();
|
||||
|
||||
// Animation-Mixer aktualisieren
|
||||
if (this.mixer) {
|
||||
this.mixer.update(delta);
|
||||
}
|
||||
|
||||
if (this.model) {
|
||||
// Sanfte Rotation
|
||||
this.model.rotation.y += 0.005;
|
||||
|
||||
// Sanftes Auf und Ab
|
||||
this.model.position.y = Math.sin(Date.now() * 0.001) * 0.1;
|
||||
}
|
||||
|
||||
if (this.renderer && this.scene && this.camera) {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
},
|
||||
|
||||
onWindowResize() {
|
||||
const container = this.$refs.container;
|
||||
if (!container || !this.camera || !this.renderer) return;
|
||||
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(width, height);
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', this.onWindowResize);
|
||||
|
||||
if (this.mixer) {
|
||||
this.mixer = null;
|
||||
}
|
||||
|
||||
if (this.model) {
|
||||
this.model.traverse((object) => {
|
||||
if (object.geometry) object.geometry.dispose();
|
||||
if (object.material) {
|
||||
if (Array.isArray(object.material)) {
|
||||
object.material.forEach(m => m.dispose());
|
||||
} else {
|
||||
object.material.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.model = null;
|
||||
}
|
||||
|
||||
if (this.renderer) {
|
||||
const container = this.$refs.container;
|
||||
if (container && this.renderer.domElement) {
|
||||
container.removeChild(this.renderer.domElement);
|
||||
}
|
||||
this.renderer.dispose();
|
||||
}
|
||||
|
||||
if (this.scene) {
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
return {
|
||||
transform: `scale(${scale})`
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -111,220 +298,7 @@ export default {
|
||||
.character-3d-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
perspective: 1000px;
|
||||
perspective-origin: center center;
|
||||
}
|
||||
|
||||
.character-3d {
|
||||
position: relative;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
transform-style: preserve-3d;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
/* Altersbasierte Anpassungen */
|
||||
.age-toddler .head {
|
||||
width: 35%;
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
.age-child .head {
|
||||
width: 32%;
|
||||
height: 27%;
|
||||
}
|
||||
|
||||
.age-teen .head {
|
||||
width: 30%;
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
.age-elderly .head {
|
||||
width: 28%;
|
||||
height: 23%;
|
||||
}
|
||||
|
||||
.age-elderly .hair {
|
||||
background: linear-gradient(135deg, #d3d3d3 0%, #a9a9a9 100%) !important;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0) rotateY(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px) rotateY(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Kopf */
|
||||
.head {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
height: 25%;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(135deg, #ffdbac 0%, #f4c2a1 100%);
|
||||
border-radius: 50% 50% 45% 45%;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.face {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.eye {
|
||||
position: absolute;
|
||||
width: 8%;
|
||||
height: 8%;
|
||||
background: #000;
|
||||
border-radius: 50%;
|
||||
top: 35%;
|
||||
}
|
||||
|
||||
.eye.left {
|
||||
left: 30%;
|
||||
}
|
||||
|
||||
.eye.right {
|
||||
right: 30%;
|
||||
}
|
||||
|
||||
.mouth {
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
height: 8%;
|
||||
left: 50%;
|
||||
top: 60%;
|
||||
transform: translateX(-50%);
|
||||
border: 2px solid #000;
|
||||
border-top: none;
|
||||
border-radius: 0 0 50% 50%;
|
||||
}
|
||||
|
||||
/* Haare */
|
||||
.hair {
|
||||
position: absolute;
|
||||
top: -15%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
border-radius: 50% 50% 0 0;
|
||||
}
|
||||
|
||||
.hair.male {
|
||||
background: linear-gradient(135deg, #2c1810 0%, #1a0f08 100%);
|
||||
clip-path: polygon(20% 0%, 80% 0%, 100% 50%, 0% 50%);
|
||||
}
|
||||
|
||||
.hair.female {
|
||||
background: linear-gradient(135deg, #ffd700 0%, #ffb347 100%);
|
||||
border-radius: 50% 50% 30% 30%;
|
||||
box-shadow: 0 -2px 10px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Körper */
|
||||
.body {
|
||||
position: absolute;
|
||||
width: 35%;
|
||||
height: 40%;
|
||||
left: 50%;
|
||||
top: 25%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 20% 20% 10% 10%;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.body.male {
|
||||
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%);
|
||||
}
|
||||
|
||||
.body.female {
|
||||
background: linear-gradient(135deg, #e24a90 0%, #c73a7a 100%);
|
||||
}
|
||||
|
||||
.chest {
|
||||
position: absolute;
|
||||
width: 60%;
|
||||
height: 40%;
|
||||
left: 50%;
|
||||
top: 20%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Arme */
|
||||
.arm {
|
||||
position: absolute;
|
||||
width: 12%;
|
||||
height: 35%;
|
||||
background: linear-gradient(135deg, #ffdbac 0%, #f4c2a1 100%);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.arm.left {
|
||||
left: 15%;
|
||||
top: 28%;
|
||||
transform: rotate(-20deg);
|
||||
}
|
||||
|
||||
.arm.right {
|
||||
right: 15%;
|
||||
top: 28%;
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
/* Beine */
|
||||
.leg {
|
||||
position: absolute;
|
||||
width: 15%;
|
||||
height: 30%;
|
||||
border-radius: 10px 10px 5px 5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.leg.left {
|
||||
left: 35%;
|
||||
top: 65%;
|
||||
}
|
||||
|
||||
.leg.right {
|
||||
right: 35%;
|
||||
top: 65%;
|
||||
}
|
||||
|
||||
.gender-male .leg {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
|
||||
}
|
||||
|
||||
.gender-female .leg {
|
||||
background: linear-gradient(135deg, #8b4caf 0%, #6b3a8f 100%);
|
||||
}
|
||||
|
||||
/* 3D-Effekt mit Schatten */
|
||||
.character-3d::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 60%;
|
||||
height: 10%;
|
||||
left: 50%;
|
||||
bottom: -5%;
|
||||
transform: translateX(-50%);
|
||||
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.3) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user