-
Notifications
You must be signed in to change notification settings - Fork 275
Description
Hello all,
i was curious about how i can make the 3D Model more realistic rendering.
So i asked Google Gemini and after some hours research and reading, endless testes and complains from me laugh it worked very nice.
My Goal was to enable Shadows, use HDR, more Light etc. you can see on the Images.
This is a Test were you can edit all settings to finetune the final look you want in your 3d Scene, after all you can then use them inside your Code.
Keep in mind, to use the functions and calls from the code inside the html file, some need to call "before" a Model gets loaded and some "after" the Model loaded.
You also can set a 360 degree Image as Background, i simply load one from CDR for testing.
Should be not hard to insert in your actual Code, or use the html file to load your Scene and edit what ever you else need to it.
If you dont need this, just ignore my post. I only wanted to share it, to give back a small part.
(And sorry that everything is written in German, i was afraid to have Gemini translate everything into English for you, as he tends to rewrite everything, even for such simple tasks. )
Here is a quick comparison, Code at the End. Looks in Real more better then on the Images:

Left Image is original, the other two are made with shadows and more light etc.
Here the Settings, you can colapse them and save Templates for later ussage too:
How to use:
create a new file named "index.html" copy the code inside, drop in the same folder a "model.glb" or change name inside code.
for local use you need to use "live-server"
Important: inside the code is a fix for Auturn Models, if you use a other Model comment this fix out or use another one e.g. for AvatarSDK
(you find it at this line: fix für avaturn modelle, if you dont need it comment this lines with // e.g.
// retarget: {
// Hips: { y: 0.03 }, Spine: { y: 0.02 }, Spine1: { y: 0.02, z: 0.01 },
// Spine2: { y: 0.02, z: 0.01 }, Neck: { z: 0.02, y: 0.01 }, Head: { z: 0.02 },
// LeftShoulder: { rx: -0.5 }, RightShoulder: { rx: -0.5 },
// scaleToHipsLevel: 1.0
// },
// baseline: {
// headRotateX: -0.04
// }
)
Code for index.html
<!DOCTYPE html>
<html>
<head>
<title>TalkingHead Pro Light Lab - All-In-One Edition</title>
<style>
body { margin: 0; background: #111; overflow: hidden; }
#avatar-container { width: 100vw; height: 100vh; }
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
"lil-gui": "https://cdn.jsdelivr.net/npm/lil-gui@0.19/+esm"
}
}
</script>
</head>
<body>
<div id="avatar-container"></div>
<script type="module">
import * as THREE from 'three';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { TalkingHead } from 'https://cdn.jsdelivr.net/gh/met4citizen/TalkingHead@1.7/modules/talkinghead.mjs';
import { GUI } from 'lil-gui';
const node = document.getElementById('avatar-container');
// --- VOLLSTÄNDIGES OPTIONS-OBJEKT ---
const options = {
modelPixelRatio: window.devicePixelRatio,
cameraView: 'upper',
toneMapping: THREE.ACESFilmicToneMapping,
exposure: 0.8,
// HDR & Hintergrund
hdrUrl: 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/venice_sunset_1k.hdr',
bgBlur: 0.5,
bgIntensity: 0.5,
envIntensity: 1.0,
groundOpacity: 0.4,
// Umgebungslicht (Ambient)
lightAmbientColor: 0xffffff,
lightAmbientIntensity: 0.8,
// Hauptlicht (Direct)
lightDirectColor: 0xfffaf0,
lightDirectIntensity: 4.0,
lightDirectPhi: 1.2,
lightDirectTheta: 0.8,
lightDirectDist: 2.0, // NEU: Abstand flexibel
// Punktlicht (Spot)
lightSpotColor: 0xffffff,
lightSpotIntensity: 12,
lightSpotPhi: 2.5,
lightSpotTheta: 3.5,
lightSpotDispersion: 0.5,
lightSpotDist: 2.0, // NEU: Abstand flexibel
// Material & Moods
skinRoughness: 0.5,
mood: 'Neutral'
};
const head = new TalkingHead(node, options);
// --- RENDERER SETUP ---
head.renderer.shadowMap.enabled = true;
head.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
head.renderer.toneMapping = options.toneMapping;
head.renderer.toneMappingExposure = options.exposure;
// --- KONTAKT-SCHATTEN BODEN ---
const groundMat = new THREE.ShadowMaterial({ opacity: options.groundOpacity });
const ground = new THREE.Mesh(new THREE.PlaneGeometry(10, 10), groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = 0;
ground.receiveShadow = true;
head.scene.add(ground);
// --- HDR SETUP ---
const rgbeLoader = new RGBELoader();
rgbeLoader.load(options.hdrUrl, (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
head.scene.environment = texture;
head.scene.background = texture;
head.scene.backgroundBlurriness = options.bgBlur;
head.scene.backgroundIntensity = options.bgIntensity;
});
// --- INITIALISIERUNG --- + fix für avaturn modelle
async function init() {
try {
await head.showAvatar({
url: './model.glb',
body: 'F',
avatarMood: 'neutral',
retarget: {
Hips: { y: 0.03 }, Spine: { y: 0.02 }, Spine1: { y: 0.02, z: 0.01 },
Spine2: { y: 0.02, z: 0.01 }, Neck: { z: 0.02, y: 0.01 }, Head: { z: 0.02 },
LeftShoulder: { rx: -0.5 }, RightShoulder: { rx: -0.5 },
scaleToHipsLevel: 1.0
},
baseline: {
headRotateX: -0.04
}
});
// Mesh-Konfiguration
head.armature.traverse(obj => {
if (obj.isMesh) {
obj.castShadow = true;
obj.receiveShadow = true;
if(obj.material) {
obj.material.shadowSide = THREE.FrontSide;
obj.material.envMapIntensity = options.envIntensity;
// Haut-Check
if(obj.material.name.toLowerCase().includes('skin')) {
obj.material.roughness = options.skinRoughness;
}
}
}
});
setupGUI();
} catch (error) {
console.error("Ladefehler:", error);
}
}
function setupGUI() {
const gui = new GUI({ title: 'TalkingHead Master Lab' });
// -------------------------------------------------------------------------
// SCENEN BELEUCHTUNG & PRESETS (Fixed Pixel Layout)
// -------------------------------------------------------------------------
const sceneFolder = gui.addFolder('Scenen Beleuchtung');
const presetConfig = {
templateName: 'Templatename',
selectedTemplate: 'Default'
};
const availableTemplates = {
'Default': JSON.parse(JSON.stringify(options)),
'High-End Studio': {
...options,
toneMapping: 4, exposure: 1.0, bgBlur: 0.8, bgIntensity: 0.4,
envIntensity: 1.5, groundOpacity: 0.6, lightAmbientIntensity: 0.3,
lightAmbientColor: 0xddddff, lightDirectIntensity: 5.0, lightDirectColor: 0xfff0dd,
lightDirectPhi: 1.3, lightDirectTheta: 0.8, lightDirectDist: 3.0,
lightSpotIntensity: 20.0, lightSpotPhi: 1.6, lightSpotTheta: 3.8,
lightSpotDispersion: 0.2, skinRoughness: 0.38
}
};
const saveFunction = () => {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(options, null, 2));
const dlNode = document.createElement('a');
dlNode.setAttribute("href", dataStr);
dlNode.setAttribute("download", (presetConfig.templateName || "scene_preset") + ".json");
document.body.appendChild(dlNode);
dlNode.click();
dlNode.remove();
};
// 1. TEXTFELD & BUTTON
const nameCtrl = sceneFolder.add(presetConfig, 'templateName');
const nameEl = nameCtrl.domElement;
// Layout Reset
nameEl.style.setProperty('--name-width', '0px');
nameEl.querySelector('.name').style.display = 'none';
const widgetEl = nameEl.querySelector('.widget');
widgetEl.style.display = 'flex';
widgetEl.style.width = '100%';
widgetEl.style.alignItems = 'center'; // Vertikal zentrieren
// INPUT: Berechnete Breite (100% - 42px für Button & Abstand)
const inputEl = widgetEl.querySelector('input');
inputEl.style.width = 'calc(100% - 42px)';
inputEl.style.minWidth = '0px';
inputEl.style.flex = 'none'; // Verhindert, dass Flexbox die Größe ändert
inputEl.style.color = '#aadd44';
inputEl.style.fontWeight = 'bold';
inputEl.style.marginRight = '2px';
// BUTTON: Feste Breite 40px
const btnEl = document.createElement('button');
btnEl.innerHTML = '💾';
btnEl.title = "Speichern";
btnEl.style.width = '40px';
btnEl.style.flex = 'none'; // Verhindert Stauchen/Dehnen
btnEl.style.height = '20px'; // Gleiche Höhe wie Input
btnEl.style.cursor = 'pointer';
btnEl.style.background = '#333';
btnEl.style.color = '#fff';
btnEl.style.border = '1px solid #555';
btnEl.style.borderRadius = '2px';
btnEl.style.lineHeight = '1';
btnEl.onclick = (e) => { e.preventDefault(); saveFunction(); };
widgetEl.appendChild(btnEl);
// 2. DROPDOWN
const dropCtrl = sceneFolder.add(presetConfig, 'selectedTemplate', Object.keys(availableTemplates));
const dropEl = dropCtrl.domElement;
// Label komplett entfernen & Platz freigeben
dropEl.style.setProperty('--name-width', '0px');
dropEl.querySelector('.name').style.display = 'none';
// Widget Container auf 100% zwingen
const dropWidget = dropEl.querySelector('.widget');
dropWidget.style.width = '100%';
dropWidget.style.minWidth = '0px'; // Wichtig für Flexbox
dropWidget.style.display = 'block'; // Block statt Flex, damit Width 100% greift
// Das Select-Element (Dropdown) auf 100% zwingen
const selectEl = dropWidget.querySelector('select');
selectEl.style.width = '100%';
selectEl.style.maxWidth = '100%';
selectEl.style.minWidth = '0px';
dropCtrl.onChange((name) => {
const template = availableTemplates[name];
if(!template) return;
Object.assign(options, template);
head.renderer.toneMapping = options.toneMapping;
head.renderer.toneMappingExposure = options.exposure;
head.scene.backgroundBlurriness = options.bgBlur;
head.scene.backgroundIntensity = options.bgIntensity;
if(groundMat) groundMat.opacity = options.groundOpacity;
head.lightAmbient.color.set(options.lightAmbientColor);
head.lightAmbient.intensity = options.lightAmbientIntensity;
head.lightDirect.color.set(options.lightDirectColor);
head.lightDirect.intensity = options.lightDirectIntensity;
head.lightSpot.color.set(options.lightSpotColor);
head.lightSpot.intensity = options.lightSpotIntensity;
head.lightSpot.penumbra = options.lightSpotDispersion;
updateLights();
head.armature.traverse(obj => {
if (obj.isMesh && obj.material) {
obj.material.envMapIntensity = options.envIntensity;
obj.material.needsUpdate = true;
if(obj.material.name.toLowerCase().includes('skin')) {
obj.material.roughness = options.skinRoughness;
}
}
});
head.setView(options.cameraView);
gui.folders.forEach(folder => {
folder.controllers.forEach(c => c.updateDisplay());
if(folder.folders) folder.folders.forEach(sub => sub.controllers.forEach(sc => sc.updateDisplay()));
});
});
sceneFolder.open();
// --- 1. KAMERA & POST-PROCESSING (VOLLSTÄNDIGE TONE MAPPING LISTE) ---
const globalFolder = gui.addFolder('Kamera & Post-Processing');
globalFolder.add(options, 'cameraView', ['full', 'mid', 'upper', 'head']).name('Ansicht').onChange(v => head.setView(v));
globalFolder.add(options, 'exposure', 0, 3).name('Belichtung (Exposure)').onChange(v => head.renderer.toneMappingExposure = v);
globalFolder.add(options, 'toneMapping', {
'NoToneMapping': THREE.NoToneMapping,
'LinearToneMapping': THREE.LinearToneMapping,
'ReinhardToneMapping': THREE.ReinhardToneMapping,
'CineonToneMapping': THREE.CineonToneMapping,
'ACESFilmicToneMapping': THREE.ACESFilmicToneMapping,
'AgXToneMapping': THREE.AgXToneMapping,
'NeutralToneMapping': THREE.NeutralToneMapping
}).name('Tone Mapping').onChange(v => {
head.renderer.toneMapping = Number(v);
head.armature.traverse(obj => { if (obj.isMesh && obj.material) obj.material.needsUpdate = true; });
});
globalFolder.close(); // Zugeklappt
// --- 2. WELT & HDR ---
const worldFolder = gui.addFolder('Welt & HDR Hintergrund');
worldFolder.add(options, 'bgBlur', 0, 1).name('Hintergrund Unschärfe').onChange(v => head.scene.backgroundBlurriness = v);
worldFolder.add(options, 'bgIntensity', 0, 2).name('Hintergrund Helligkeit').onChange(v => head.scene.backgroundIntensity = v);
worldFolder.add(options, 'envIntensity', 0, 3).name('HDR Reflexions-Stärke').onChange(v => {
head.armature.traverse(obj => { if (obj.isMesh && obj.material) obj.material.envMapIntensity = v; });
});
worldFolder.add(options, 'groundOpacity', 0, 1).name('Bodenschatten Stärke').onChange(v => groundMat.opacity = v);
worldFolder.close(); // Zugeklappt
// --- 3. UMGEBUNGSLICHT (VOLLSTÄNDIG) ---
const ambientFolder = gui.addFolder('1. Umgebungslicht');
ambientFolder.addColor(options, 'lightAmbientColor').name('Farbe').onChange(v => head.lightAmbient.color.set(v));
ambientFolder.add(options, 'lightAmbientIntensity', 0, 5).name('Intensität').onChange(v => head.lightAmbient.intensity = v);
ambientFolder.close(); // Zugeklappt
// --- 4. HAUPTLICHT (VOLLSTÄNDIG) ---
const directFolder = gui.addFolder('2. Hauptlicht (Sonne)');
directFolder.addColor(options, 'lightDirectColor').name('Farbe').onChange(v => head.lightDirect.color.set(v));
directFolder.add(options, 'lightDirectIntensity', 0, 20).name('Intensität').onChange(v => head.lightDirect.intensity = v);
// NEU: Erweiterte Steuerung
directFolder.add(options, 'lightDirectPhi', 0, Math.PI).name('Winkel Höhe (Phi)').onChange(() => updateLights());
directFolder.add(options, 'lightDirectTheta', 0, Math.PI * 2).name('Winkel Kreis (Theta)').onChange(() => updateLights());
directFolder.add(options, 'lightDirectDist', 0.1, 10).name('Abstand (Radius)').onChange(() => updateLights());
// Manuelle XYZ Position (nur .listen() damit es sich aktualisiert, falls Phi/Theta genutzt werden)
const directPos = directFolder.addFolder('Manuelle Position (XYZ)');
directPos.add(head.lightDirect.position, 'x', -5, 5).name('Pos X').listen();
directPos.add(head.lightDirect.position, 'y', -5, 5).name('Pos Y').listen();
directPos.add(head.lightDirect.position, 'z', -5, 5).name('Pos Z').listen();
// --- 5. SPOTLICHT (VOLLSTÄNDIG) ---
const spotFolder = gui.addFolder('3. Rim-Light (Kontur)');
spotFolder.addColor(options, 'lightSpotColor').name('Farbe').onChange(v => head.lightSpot.color.set(v));
spotFolder.add(options, 'lightSpotIntensity', 0, 100).name('Intensität').onChange(v => head.lightSpot.intensity = v);
spotFolder.add(options, 'lightSpotDispersion', 0, 1).name('Weichheit (Penumbra)').onChange(v => head.lightSpot.penumbra = v);
// NEU: Erweiterte Steuerung
spotFolder.add(options, 'lightSpotPhi', 0, Math.PI).name('Winkel Höhe (Phi)').onChange(() => updateLights());
spotFolder.add(options, 'lightSpotTheta', 0, Math.PI * 2).name('Winkel Kreis (Theta)').onChange(() => updateLights());
spotFolder.add(options, 'lightSpotDist', 0.1, 10).name('Abstand (Radius)').onChange(() => updateLights());
// Manuelle XYZ
const spotPos = spotFolder.addFolder('Manuelle Position (XYZ)');
spotPos.add(head.lightSpot.position, 'x', -5, 5).name('Pos X').listen();
spotPos.add(head.lightSpot.position, 'y', -5, 5).name('Pos Y').listen();
spotPos.add(head.lightSpot.position, 'z', -5, 5).name('Pos Z').listen();
// --- 6. HAUT & STIMMUNG (VOLLSTÄNDIG) ---
const skinFolder = gui.addFolder('Haut & Emotionale Moods');
skinFolder.add(options, 'skinRoughness', 0, 1).name('Haut Rauheit').onChange(v => {
head.armature.traverse(obj => {
if (obj.isMesh && obj.material && obj.material.name.toLowerCase().includes('skin')) obj.material.roughness = v;
});
});
const moods = {
'Neutral': { ambient: 0xffffff, direct: 0xfffaf0, intensity: 4 },
'Happy/Sun': { ambient: 0xffeebb, direct: 0xffd1a4, intensity: 6 },
'Serious/Cyber': { ambient: 0x3300ff, direct: 0x00ffcc, intensity: 3 },
'Sad/Night': { ambient: 0x111144, direct: 0x4444ff, intensity: 1.5 },
'Angry/Red': { ambient: 0x440000, direct: 0xff0000, intensity: 5 }
};
skinFolder.add(options, 'mood', Object.keys(moods)).name('Mood Preset').onChange(v => {
const m = moods[v];
head.lightAmbient.color.set(m.ambient);
head.lightDirect.color.set(m.direct);
head.lightDirect.intensity = m.intensity;
});
skinFolder.close(); // Zugeklappt
function updateLights() {
// Hier wird nun der dynamische Abstand verwendet statt fest "2"
head.lightDirect.position.setFromSphericalCoords(options.lightDirectDist, options.lightDirectPhi, options.lightDirectTheta);
head.lightSpot.position.setFromSphericalCoords(options.lightSpotDist, options.lightSpotPhi, options.lightSpotTheta);
}
// --- SCHATTEN-SETUPS ---
head.lightDirect.castShadow = true;
head.lightDirect.shadow.bias = -0.0005;
head.lightDirect.shadow.mapSize.set(2048, 2048);
head.lightDirect.shadow.camera.top = 2;
head.lightDirect.shadow.camera.bottom = -1;
head.lightDirect.shadow.camera.left = -1;
head.lightDirect.shadow.camera.right = 1;
updateLights();
}
init();
</script>
</body>
</html>
