需求背景与问题
公司要求做一个能够不同角度查看 GLB 模型并能够获取到截图,根据粗模的截图后续交由Stable Diffusion进行渲染。
目前采用的控制器为 PointerLockControls,后续不确定是否重构为 FlyControls进行第一人称控制,现在是自行监听键盘。
功能开发后发现了两个问题
- 获取canvas渲染的图像是黑屏的
- 截图交给SD于插件识别轮廓进行 ControlNet 生成渲染图片,但由于光照问题导致物体识别效果不好,没有识别出轮廓线
基本代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
| <template> <div class="relative w-full"> <canvas ref="containerRef" class="relative w-full h-full aspect-video ..." /> </div> </template>
<script setup lang="ts"> import { ref, onMounted, onBeforeUnmount, watch } from "vue"; import * as THREE from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { PointerLockControls } from "three/addons/controls/PointerLockControls.js"; import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
const props = withDefaults(defineProps<Props>(), { modelUrl: "", });
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let sceneBackground: string = "#cfcfcf"; let renderer: THREE.WebGLRenderer;
let controls: PointerLockControls;
let loadedModel: THREE.Group | null = null;
const _direction = new THREE.Vector3(); const _right = new THREE.Vector3(); const _moveVector = new THREE.Vector3();
function initScene() { .... }
function loadModel(url: string) { if (!url || !url.trim()) { console.warn("模型URL为空,跳过加载"); isLoading.value = false; return; } const loader = new GLTFLoader(); const dracoLoader = new DRACOLoader(); dracoLoader.setDecoderPath( "https://www.gstatic.com/draco/versioned/decoders/1.5.6/" ); dracoLoader.setDecoderConfig({ type: "wasm" }); loader.setDRACOLoader(dracoLoader);
isLoading.value = true;
loader.load( url, (gltf: any) => { if (loadedModel) { scene.remove(loadedModel); } const model = gltf.scene; loadedModel = model; scene.add(model); fitCameraToObject(camera, model); }, (xhr: any) => { ...}, (error: any) => { ... } ); }
const takeScreenshot = (): string => { if (!renderer) return ""; console.log("执行截图函数"); const dataURL = renderer.domElement.toDataURL("image/png"); return dataURL; };
function animate() { animationFrameId = requestAnimationFrame(animate); updateCameraPosition(); renderer.render(scene, camera); }
...
onMounted(() => initScene(); if (props.modelUrl && props.modelUrl.trim()) { loadModel(props.modelUrl); } animate(); ... }); </script>
|
解决方式
关于渲染问题通过查阅很快就找到了问题,因为截图执行的时候可能由于在未渲染的那一帧或调度等因素导致截图前当前帧没有进行渲染,只需要在截图的前强制再渲染一帧即可。
1 2 3 4 5 6 7 8 9
| const takeScreenshot = (): string => { if (!renderer) return ""; console.log("执行截图函数"); renderer.render(scene, camera); const dataURL = renderer.domElement.toDataURL("image/png"); return dataURL; };
|
不过关于光照问题尝试多添加光源仍然不能很好的解决。
故尝试直接在渲染的时候直接把模型的轮廓渲染出来,但是效果依然不好,Three.js没办法做到内角的线条渲染,所以还需要能够透视。
不过有的时候其实并不需要线条,所以给出一个开关来控制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const enableOutline = ref(false);
const disableDepthTest = ref(false);
watch([enableOutline, disableDepthTest], () => { loadedModel.traverse((child: THREE.Object3D) => { if ((child as THREE.Mesh).isMesh) { const edge = new THREE.EdgesGeometry( (child as THREE.Mesh).geometry, ); const lineMaterial = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2, depthWrite: false, depthTest: !disableDepthTest.value, }); const line = new THREE.LineSegments(edge, lineMaterial); line.renderOrder = 1; child.add(line); } }); });
|
但是发现又有了一个新的问题,这段代码没有办法发挥该有的作用,线一打开就没办法关闭了,而关闭又需要再次获取到轮廓线对象,又需要额外的管理line,也需要在到 watch 中再进行判断,并且这样又需要再频繁创建和销毁对象会有额外的不必要开销。
解决轮廓线控制
后面发现物体的对象其实是可以控制可见性的,那我们只需要一开始就创建,后续只需要控制可见性就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
|
watch([enableOutline, disableDepthTest], () => { if (loadedModel && isModelLoaded.value) { loadedModel.traverse((child: THREE.Object3D) => { if ((child as THREE.Mesh).isMesh) { const line = child.children.find( (c) => (c as THREE.LineSegments).isLineSegments ) as THREE.LineSegments; if (line) { line.visible = enableOutline.value; (line.material as THREE.LineBasicMaterial).depthTest = !disableDepthTest.value; } } }); } });
function loadModel(url: string) { if (!url || !url.trim()) { console.warn("模型URL为空,跳过加载"); isLoading.value = false; return; }
const loader = new GLTFLoader();
....
isLoading.value = true;
loader.load( url, (gltf: any) => { ...... model.traverse((child: THREE.Object3D) => { if ((child as THREE.Mesh).isMesh) { const edge = new THREE.EdgesGeometry( (child as THREE.Mesh).geometry, ); const lineMaterial = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2, depthWrite: false, depthTest: !disableDepthTest.value, }); const line = new THREE.LineSegments(edge, lineMaterial); line.renderOrder = 1; line.visible = enableOutline.value; child.add(line); } });
fitCameraToObject(camera, model); }, (xhr: any) => {...}, (error: any) => {...} ); }
|