felixmariotto / three-mesh-ui

⏹ Make VR user interfaces for Three.js
https://felixmariotto.github.io/three-mesh-ui/#basic_setup
MIT License
1.26k stars 134 forks source link

Weird background behavior after Block remove and add #225

Closed michal-repo closed 1 year ago

michal-repo commented 1 year ago

Hi,

I have weird background behavior after Block is removed from button and replaced with second one. Inner block is using InlineBlock with backgroundTexture.

Before click: obraz

After click: obraz

It doesn't matter if I use InlineBlock or Block for object with texture background.

But I can see that if I add second "dummy" button and add unmuteIconElement to it (without adding it to panel) texture is showing fine, without darker background.

obraz

Is this a bug or normal behavior and I'm missing something?

Example code: ``` import * as THREE from './node_modules/three/build/three.module.js'; import { VRButton } from './node_modules/three/examples/jsm/webxr/VRButton.js'; import { OrbitControls } from './node_modules/three/examples/jsm/controls/OrbitControls.js'; import ThreeMeshUI from './node_modules/three-mesh-ui/build/three-mesh-ui.module.js'; import VRControl from './node_modules/three-mesh-ui/examples/utils/VRControl.js'; import FontJSON from './node_modules/three-mesh-ui/examples/assets/Roboto-msdf.json'; import FontImage from './node_modules/three-mesh-ui/examples/assets/Roboto-msdf.png'; // Import Icons import MuteIcon from './assets/icons/mute.png'; import VolumeIcon from './assets/icons/volume.png'; let scene, camera, renderer, controls, vrControl; let objsToTest = []; let unmuteIconElement, muteIconElement; const loader = new THREE.TextureLoader(); window.addEventListener('load', init); window.addEventListener('resize', onWindowResize); // compute mouse position in normalized device coordinates // (-1 to +1) for both directions. // Used to raycasting against the interactive elements const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); mouse.x = mouse.y = null; let selectState = false; window.addEventListener('pointermove', (event) => { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; }); window.addEventListener('pointerdown', () => { selectState = true; }); window.addEventListener('pointerup', () => { selectState = false; }); window.addEventListener('touchstart', (event) => { selectState = true; mouse.x = (event.touches[0].clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.touches[0].clientY / window.innerHeight) * 2 + 1; }); window.addEventListener('touchend', () => { selectState = false; mouse.x = null; mouse.y = null; }); // function init() { //////////////////////// // Basic Three Setup //////////////////////// scene = new THREE.Scene(); scene.background = new THREE.Color(0x101010); camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 1, 1000); camera.position.y = 1.6; // camera.position.set(0, 1.6, 0); renderer = new THREE.WebGLRenderer({ antialias: true }); // renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); // renderer.outputEncoding = THREE.sRGBEncoding; renderer.xr.enabled = true; // renderer.xr.setReferenceSpaceType('local'); document.body.appendChild(VRButton.createButton(renderer)); document.body.appendChild(renderer.domElement); // Orbit controls for no-vr controls = new OrbitControls(camera, renderer.domElement); controls.target = new THREE.Vector3(0, 1, -1.8); ////////// // Light ////////// const color = 0xFFFFFF; const intensity = 1; const light = new THREE.DirectionalLight(color, intensity); light.position.set(-1, 2, 4); scene.add(light); //////////////// // Controllers //////////////// vrControl = VRControl(renderer); scene.add(vrControl.controllerGrips[0], vrControl.controllers[0]); vrControl.controllers[0].addEventListener('selectstart', () => { selectState = true; }); vrControl.controllers[0].addEventListener('selectend', () => { selectState = false; }); ////////// // Panel ////////// makePanel(); // renderer.setAnimationLoop(loop); } /////////////////// // UI contruction /////////////////// function makePanel() { const menuContainer = new ThreeMeshUI.Block({ justifyContent: 'center', contentDirection: 'column-reverse', fontFamily: FontJSON, fontTexture: FontImage, fontSize: 0.07, padding: 0.02, borderRadius: 0, width: 2 }); const menuContainerButtons = new ThreeMeshUI.Block({ justifyContent: 'center', contentDirection: 'row', fontFamily: FontJSON, fontTexture: FontImage, fontSize: 0.07, padding: 0.02, borderRadius: 0.11, backgroundOpacity: 0 }); menuContainer.position.set(0, 1.2, -1.6); menuContainer.add(menuContainerButtons); // BUTTONS const buttonOptions = { width: 0.15, height: 0.15, justifyContent: 'center', offset: 0.05, margin: 0.02, borderRadius: 0.08 }; // Options for component.setupState(). // It must contain a 'state' parameter, which you will refer to with component.setState( 'name-of-the-state' ). const hoveredStateAttributes = { state: 'hovered', attributes: { offset: 0.035, backgroundColor: new THREE.Color(0xffff00), backgroundOpacity: 1, fontColor: new THREE.Color(0x000000) }, }; const idleStateAttributes = { state: 'idle', attributes: { offset: 0.035, backgroundColor: new THREE.Color(0x999999), backgroundOpacity: 0.5, fontColor: new THREE.Color(0xffffff) }, }; // Buttons creation, with the options objects passed in parameters. const buttonMute = new ThreeMeshUI.Block(buttonOptions); muteIconElement = new ThreeMeshUI.Block({ width: 0.15, height: 0.15, justifyContent: 'center', backgroundOpacity: 0, offset: 0, margin: 0.02, borderRadius: 0.08 }); unmuteIconElement = new ThreeMeshUI.Block({ width: 0.15, height: 0.15, justifyContent: 'center', backgroundOpacity: 0, offset: 0, margin: 0.02, borderRadius: 0.08 }); loader.load(MuteIcon, (texture) => { muteIconElement.add( new ThreeMeshUI.InlineBlock({ height: 0.1, width: 0.1, backgroundTexture: texture, borderRadius: 0 }) ); }); loader.load(VolumeIcon, (texture) => { unmuteIconElement.add( new ThreeMeshUI.InlineBlock({ height: 0.1, width: 0.1, backgroundTexture: texture, borderRadius: 0 }) ); }); buttonMute.add(muteIconElement); buttonMute.muted = false; // Create states for the buttons. // In the loop, we will call component.setState( 'state-name' ) when mouse hover or click const selectedAttributes = { offset: 0.02, backgroundColor: new THREE.Color(0x777777), fontColor: new THREE.Color(0x222222) }; // buttonMute.setupState({ state: 'selected', attributes: selectedAttributes, onSet: () => { switch (buttonMute.muted) { case true: buttonMute.muted = false; buttonMute.remove(unmuteIconElement); buttonMute.add(muteIconElement); break; case false: buttonMute.muted = true; buttonMute.remove(muteIconElement); buttonMute.add(unmuteIconElement); break; } } }); buttonMute.setupState(hoveredStateAttributes); buttonMute.setupState(idleStateAttributes); // menuContainerButtons.add(buttonMute); objsToTest.push(buttonMute); scene.add(menuContainer); } // Handle resizing the viewport function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } // function loop() { // Don't forget, ThreeMeshUI must be updated manually. // This has been introduced in version 3.0.0 in order // to improve performance ThreeMeshUI.update(); controls.update(); renderer.render(scene, camera); updateButtons(); } // Called in the loop, get intersection with either the mouse or the VR controllers, // then update the buttons states according to result function updateButtons() { // Find closest intersecting object let intersect; if (renderer.xr.isPresenting) { vrControl.setFromController(0, raycaster.ray); intersect = raycast(); // Position the little white dot at the end of the controller pointing ray // need to skip this if intersecting hiddenSphere because in VR it spreads points apart if (intersect) { vrControl.setPointerAt(0, intersect.point); }; } else if (mouse.x !== null && mouse.y !== null) { raycaster.setFromCamera(mouse, camera); intersect = raycast(); } // Update targeted button state (if any) if (intersect && intersect.object.isUI) { if (selectState) { // Component.setState internally call component.set with the options you defined in component.setupState intersect.object.setState('selected'); } else { // Component.setState internally call component.set with the options you defined in component.setupState intersect.object.setState('hovered'); } } // Update non-targeted buttons state objsToTest.forEach((obj) => { if ((!intersect || obj !== intersect.object) && obj.isUI) { // Component.setState internally call component.set with the options you defined in component.setupState obj.setState('idle'); } }); } // function raycast() { return objsToTest.reduce((closestIntersection, obj) => { const intersection = raycaster.intersectObject(obj, true); if (!intersection[0]) return closestIntersection; if (!closestIntersection || intersection[0].distance < closestIntersection.distance) { intersection[0].object = obj; return intersection[0]; } return closestIntersection; }, null); } ```
Example code with dummy button: ``` import * as THREE from './node_modules/three/build/three.module.js'; import { VRButton } from './node_modules/three/examples/jsm/webxr/VRButton.js'; import { OrbitControls } from './node_modules/three/examples/jsm/controls/OrbitControls.js'; import ThreeMeshUI from './node_modules/three-mesh-ui/build/three-mesh-ui.module.js'; import VRControl from './node_modules/three-mesh-ui/examples/utils/VRControl.js'; import FontJSON from './node_modules/three-mesh-ui/examples/assets/Roboto-msdf.json'; import FontImage from './node_modules/three-mesh-ui/examples/assets/Roboto-msdf.png'; // Import Icons import MuteIcon from './assets/icons/mute.png'; import VolumeIcon from './assets/icons/volume.png'; let scene, camera, renderer, controls, vrControl; let objsToTest = []; let unmuteIconElement, muteIconElement; const loader = new THREE.TextureLoader(); window.addEventListener('load', init); window.addEventListener('resize', onWindowResize); // compute mouse position in normalized device coordinates // (-1 to +1) for both directions. // Used to raycasting against the interactive elements const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); mouse.x = mouse.y = null; let selectState = false; window.addEventListener('pointermove', (event) => { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; }); window.addEventListener('pointerdown', () => { selectState = true; }); window.addEventListener('pointerup', () => { selectState = false; }); window.addEventListener('touchstart', (event) => { selectState = true; mouse.x = (event.touches[0].clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.touches[0].clientY / window.innerHeight) * 2 + 1; }); window.addEventListener('touchend', () => { selectState = false; mouse.x = null; mouse.y = null; }); // function init() { //////////////////////// // Basic Three Setup //////////////////////// scene = new THREE.Scene(); scene.background = new THREE.Color(0x101010); camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 1, 1000); camera.position.y = 1.6; // camera.position.set(0, 1.6, 0); renderer = new THREE.WebGLRenderer({ antialias: true }); // renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); // renderer.outputEncoding = THREE.sRGBEncoding; renderer.xr.enabled = true; // renderer.xr.setReferenceSpaceType('local'); document.body.appendChild(VRButton.createButton(renderer)); document.body.appendChild(renderer.domElement); // Orbit controls for no-vr controls = new OrbitControls(camera, renderer.domElement); controls.target = new THREE.Vector3(0, 1, -1.8); ////////// // Light ////////// const color = 0xFFFFFF; const intensity = 1; const light = new THREE.DirectionalLight(color, intensity); light.position.set(-1, 2, 4); scene.add(light); //////////////// // Controllers //////////////// vrControl = VRControl(renderer); scene.add(vrControl.controllerGrips[0], vrControl.controllers[0]); vrControl.controllers[0].addEventListener('selectstart', () => { selectState = true; }); vrControl.controllers[0].addEventListener('selectend', () => { selectState = false; }); ////////// // Panel ////////// makePanel(); // renderer.setAnimationLoop(loop); } /////////////////// // UI contruction /////////////////// function makePanel() { const menuContainer = new ThreeMeshUI.Block({ justifyContent: 'center', contentDirection: 'column-reverse', fontFamily: FontJSON, fontTexture: FontImage, fontSize: 0.07, padding: 0.02, borderRadius: 0, width: 2 }); const menuContainerButtons = new ThreeMeshUI.Block({ justifyContent: 'center', contentDirection: 'row', fontFamily: FontJSON, fontTexture: FontImage, fontSize: 0.07, padding: 0.02, borderRadius: 0.11, backgroundOpacity: 0 }); menuContainer.position.set(0, 1.2, -1.6); menuContainer.add(menuContainerButtons); // BUTTONS const buttonOptions = { width: 0.15, height: 0.15, justifyContent: 'center', offset: 0.05, margin: 0.02, borderRadius: 0.08 }; // Options for component.setupState(). // It must contain a 'state' parameter, which you will refer to with component.setState( 'name-of-the-state' ). const hoveredStateAttributes = { state: 'hovered', attributes: { offset: 0.035, backgroundColor: new THREE.Color(0xffff00), backgroundOpacity: 1, fontColor: new THREE.Color(0x000000) }, }; const idleStateAttributes = { state: 'idle', attributes: { offset: 0.035, backgroundColor: new THREE.Color(0x999999), backgroundOpacity: 0.5, fontColor: new THREE.Color(0xffffff) }, }; // Buttons creation, with the options objects passed in parameters. const buttonMute = new ThreeMeshUI.Block(buttonOptions); const buttonMute2 = new ThreeMeshUI.Block(buttonOptions); muteIconElement = new ThreeMeshUI.Block({ width: 0.15, height: 0.15, justifyContent: 'center', backgroundOpacity: 0, offset: 0, margin: 0.02, borderRadius: 0.08 }); unmuteIconElement = new ThreeMeshUI.Block({ width: 0.15, height: 0.15, justifyContent: 'center', backgroundOpacity: 0, offset: 0, margin: 0.02, borderRadius: 0.08 }); loader.load(MuteIcon, (texture) => { muteIconElement.add( new ThreeMeshUI.Block({ height: 0.1, width: 0.1, backgroundTexture: texture, borderRadius: 0 }) ); }); loader.load(VolumeIcon, (texture) => { unmuteIconElement.add( new ThreeMeshUI.Block({ height: 0.1, width: 0.1, backgroundTexture: texture, borderRadius: 0 }) ); }); buttonMute.add(muteIconElement); buttonMute.muted = false; buttonMute2.add(unmuteIconElement); // Create states for the buttons. // In the loop, we will call component.setState( 'state-name' ) when mouse hover or click const selectedAttributes = { offset: 0.02, backgroundColor: new THREE.Color(0x777777), fontColor: new THREE.Color(0x222222) }; // buttonMute.setupState({ state: 'selected', attributes: selectedAttributes, onSet: () => { switch (buttonMute.muted) { case true: buttonMute.muted = false; buttonMute.remove(unmuteIconElement); buttonMute.add(muteIconElement); break; case false: buttonMute.muted = true; buttonMute.remove(muteIconElement); buttonMute.add(unmuteIconElement); break; } } }); buttonMute.setupState(hoveredStateAttributes); buttonMute.setupState(idleStateAttributes); // menuContainerButtons.add(buttonMute); objsToTest.push(buttonMute); scene.add(menuContainer); } // Handle resizing the viewport function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } // function loop() { // Don't forget, ThreeMeshUI must be updated manually. // This has been introduced in version 3.0.0 in order // to improve performance ThreeMeshUI.update(); controls.update(); renderer.render(scene, camera); updateButtons(); } // Called in the loop, get intersection with either the mouse or the VR controllers, // then update the buttons states according to result function updateButtons() { // Find closest intersecting object let intersect; if (renderer.xr.isPresenting) { vrControl.setFromController(0, raycaster.ray); intersect = raycast(); // Position the little white dot at the end of the controller pointing ray // need to skip this if intersecting hiddenSphere because in VR it spreads points apart if (intersect) { vrControl.setPointerAt(0, intersect.point); }; } else if (mouse.x !== null && mouse.y !== null) { raycaster.setFromCamera(mouse, camera); intersect = raycast(); } // Update targeted button state (if any) if (intersect && intersect.object.isUI) { if (selectState) { // Component.setState internally call component.set with the options you defined in component.setupState intersect.object.setState('selected'); } else { // Component.setState internally call component.set with the options you defined in component.setupState intersect.object.setState('hovered'); } } // Update non-targeted buttons state objsToTest.forEach((obj) => { if ((!intersect || obj !== intersect.object) && obj.isUI) { // Component.setState internally call component.set with the options you defined in component.setupState obj.setState('idle'); } }); } // function raycast() { return objsToTest.reduce((closestIntersection, obj) => { const intersection = raycaster.intersectObject(obj, true); if (!intersection[0]) return closestIntersection; if (!closestIntersection || intersection[0].distance < closestIntersection.distance) { intersection[0].object = obj; return intersection[0]; } return closestIntersection; }, null); } ```
swingingtom commented 1 year ago

Hi @michal-repo, Thanks for you issue, it's level of details is nice.

It doesn't seem uncommon. I would go for a render order / depth issue.

What you called "darker background" is actually the scene background. The unmute icon actually discard transparent pixels, and the order / depth issue shows the scene background instead of the unmute parent/container background.

You could play with material.alphaTest to prevent discarding transparent pixels or .set({offset:x}) / .renderOrder = x; to solve the render order issue itself.

I ain't lot of free time right now to build it myself, but if you can build a codesanbox or similar, may be the community could offer you one or more solutions.