Skip to content

[ADDON] TalkingHead Pro Light Lab - All-In-One Edition #158

@CustomCoder94

Description

@CustomCoder94

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:
Image

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:

Image

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>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions