技术: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.LinearFilter
child.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.01
selectedImage.scaleX += 0.01
}else if(event.key === '7'){
selectedImage.scaleY -= 0.01
selectedImage.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 = true
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
// 鼠标点击事件
function onModelClick(event) {
flag.x = false
event.preventDefault();
// pos 在场景图像上的位置
var pos = [event.clientX,event.clientY]
var rect = container.getBoundingClientRect();
mouse.x = ((pos[0] - rect.left) / rect.width) *2-1
mouse.y = -((pos[1] - rect.top) / rect.height) *2+1
raycaster.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表示画布的宽和高都是512
var 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 = obj
s.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各个位置的缩放,还是很有趣的~