技术:threejs+canvas+fabric
效果图:
原理:threejs中没有局部贴图的效果,只能通过map 的方式贴到模型上,所以说换一种方式来实现,通过canvas+fabric来实现图片的移动缩放旋转,然后将整个画布以map 的形式放到模型材质上,实现局部贴图的效果
直接上代码:
<template><div id="c-left"><input type="file" @change="handleFileChange" accept=".png" /><div id="container"></div></div><div id="c-right"><canvas id="canvas" width="512" height="512"></canvas></div>
</template><script>
import { fabric } from 'fabric'
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';// oss上传相关配置
let OSS = require('ali-oss')
let client = new OSS({region: 'oss-cn-beijing',accessKeyId: 'xxxxx',accessKeySecret: 'xxxxx',bucket: 'xxxxx'
})// 设置场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xfffff0);
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
scene.add(ambientLight);
const dirLight1 = new THREE.DirectionalLight( 0xffffff, 2.5);
dirLight1.position.set( 0, 0.5, 1 );
scene.add( dirLight1 );const dirLight2 = new THREE.DirectionalLight( 0xffffff, 2.5);
dirLight2.position.set( 0, 0.5, -1 );
scene.add( dirLight2 );const dirLight3 = new THREE.DirectionalLight( 0xffffff, 2.5 );
dirLight3.position.set( 0, -0.5, 0 );
scene.add( dirLight3 );const n = 2
// 设置视角
const camera = new THREE.PerspectiveCamera(75,window.innerWidth/n / window.innerHeight,0.1,1000
);
camera.position.set(0, 5, 10);
// 随机名称
function generateRandomFileName() {const date = new Date().toISOString().replace(/[-:.TZ]/g, '');const randomPart = Math.random().toString(36).substr(2, 6);return `${date}-${randomPart}`;
}let selectedImage = null
export default {data(){return {canvas_s:null,image_url:null,}},methods:{async handleFileChange(event) {const file = event.target.files[0];if (!file || file.type!== 'image/png') {alert('请选择 PNG 格式的图片!');return;}const fileName = generateRandomFileName();await client.put(`m2_photos/${fileName}`, file);const url = client.signatureUrl(`m2_photos/${fileName}`);console.log("url为: ", url);this.image_url = url},init(){let flag = {x:false}; // 创建渲染器const renderer = new THREE.WebGLRenderer({preserveDrawingBuffer: true,antialias: true,});const container = document.getElementById("container");container.appendChild(renderer.domElement);var s = new fabric.Canvas('canvas');s.backgroundColor = 'rgb(100, 255, 255)'; // 设置画布背景this.canvas_s = s// 创建轨道控制器const controls = new OrbitControls(camera, renderer.domElement);renderer.shadowMap.enabled = true;renderer.shadowMap.type = THREE.PCFSoftShadowMap;renderer.outputEncoding = THREE.sRGBEncoding;// 开启场景中的阴影贴图renderer.shadowMap.enabled = true;// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。controls.enableDamping = true;renderer.setSize(window.innerWidth/n, window.innerHeight);// 添加坐标系const axesHelper = new THREE.AxesHelper(10);scene.add(axesHelper);// 异步添加图片,能够实现图片的任意交互fabric.Image.fromURL('xxxxxxx', (oImg)=> {oImg.scale(0.1);var canvasWidth = s.width;var canvasHeight = s.height;// 计算图片放置在正中间的位置var left = canvasWidth / 2 ;var top = canvasHeight / 2 ;oImg.set({left: left - 80, top: top -40 });console.log("oImg : ",oImg);s.add(oImg);}, {crossOrigin: 'anonymous'});// 定时任务setInterval(()=>{if (this.image_url) {fabric.Image.fromURL(this.image_url, (oImg)=> {oImg.scale(0.1);var canvasWidth = s.width;var canvasHeight = s.height;// 计算图片放置在正中间的位置var left = canvasWidth / 2 ;var top = canvasHeight / 2 ;oImg.set({left: left - 80, top: top -40 });console.log("oImg : ",oImg);s.add(oImg);}, {crossOrigin: 'anonymous'});this.image_url = null}},1000)var texture = new THREE.Texture(document.getElementById("canvas"));texture.anisotropy = renderer.capabilities.getMaxAnisotropy();const mapTexture = new THREE.TextureLoader().load('/statisc/fabric004.png')const loader = new OBJLoader();loader.load('模型的位置', (object) => {object.traverse((child) => {child.material = new THREE.MeshLambertMaterial({ color:0xffffff,side:THREE.DoubleSide,// transparent:false,// opacity:1,bumpMap:mapTexture,// alphaMap:mapTexture,bumpScale:1,// emissive:0x404040});child.material.map = texture;child.material.map.minFilter = THREE.LinearFilterchild.material.map.colorSpace = 'srgb'console.log("map",child.material.map);});object.scale.set(0.1, 0.1, 0.1); // 变小一点object.position.set(0, -10, 0)scene.add(object);// 新增:为模型添加点击事件监听renderer.domElement.addEventListener('click', onModelClick);}, () => {}, () => {});// 按键设置document.addEventListener('keydown',function (event) {if (flag.x) {if (event.key === 's') {selectedImage.top += 5;}else if(event.key === 'a'){selectedImage.left -= 5;}else if( event.key === 'd'){selectedImage.left += 5;}else if(event.key === 'w'){selectedImage.top -= 5;}else if(event.key === 'q'){selectedImage.angle -= 5}else if(event.key === 'e'){selectedImage.angle += 5}else if(event.key === '6'){selectedImage.scaleX += 0.01}else if(event.key === '4'){selectedImage.scaleX -= 0.01}else if(event.key === '2'){selectedImage.scaleY += 0.01}else if(event.key === '8'){selectedImage.scaleY -= 0.01}else if(event.key === '3'){selectedImage.scaleY += 0.01selectedImage.scaleX += 0.01}else if(event.key === '7'){selectedImage.scaleY -= 0.01selectedImage.scaleX -= 0.01}else if(event.key === 'Backspace'){s.remove(selectedImage)}else if(event.key === 'ArrowUp'){s.bringForward(selectedImage)}else if(event.key === 'ArrowDown'){s.sendBackwards(selectedImage)}s.renderAll();}})const geometry = new THREE.BoxGeometry(1, 1, 1);const material = new THREE.MeshBasicMaterial({ map:texture });const cube = new THREE.Mesh(geometry, material);scene.add(cube);function render() {controls.update();texture.needsUpdate = truerenderer.render(scene, camera);// 渲染下一帧的时候就会调用render函数requestAnimationFrame(render);}render();var raycaster = new THREE.Raycaster();var mouse = new THREE.Vector2();// 鼠标点击事件function onModelClick(event) { flag.x = falseevent.preventDefault();// pos 在场景图像上的位置var pos = [event.clientX,event.clientY]var rect = container.getBoundingClientRect();mouse.x = ((pos[0] - rect.left) / rect.width) *2-1mouse.y = -((pos[1] - rect.top) / rect.height) *2+1raycaster.setFromCamera(mouse, camera);// 通过射线获得场景中的对象var intersects = raycaster.intersectObjects(scene.children);if (intersects.length > 0 && intersects[0].uv) {var uv = intersects[0].uv;intersects[0].object.material.map.transformUv(uv)// 512表示画布的宽和高都是512var x = Math.round(uv.x * rect.width/(1+0.002*(rect.width-512))); var y = Math.round(uv.y * rect.height/(1+0.002*(rect.height-512)));const positionOnScene = {x,y}selectCanvas(positionOnScene,flag)}if (!flag.x) {s.discardActiveObject();s.renderAll();}}// 选中模型中的图片function selectCanvas(point,flag) {const objects = s.getObjects();for (let i = objects.length - 1; i >= 0; i--) {const obj = objects[i];if (obj.containsPoint(point)) {s.setActiveObject(obj); // 设置图形为选中状态flag.x = true; // 标记有图形被选中selectedImage = objs.renderAll();break; }}}}},mounted() {this.init();},
}</script><style>
#c-left, #c-right {
position: relative;
display: inline-block;
height: 100%;
width: 50%;
}#c-right {
float: right;
/* display: none; */
}
</style>
我是使用的vue3,同时还包含了oss的图片上传功能以及threejs 的反射效果,当点击模型上的图片时,即可选中图片,并通过wasd移动图片位置,qe旋转,123456789各个位置的缩放,还是很有趣的~