Fix billboards

This commit is contained in:
JannisX11 2025-03-14 22:45:40 +01:00
parent 348dd449ff
commit 43c34a8db5
6 changed files with 141 additions and 64 deletions

View File

@ -59,6 +59,7 @@ import "./outliner/collections"
import "./outliner/group"
import "./outliner/mesh"
import "./outliner/cube"
import "./outliner/billboard"
import "./outliner/texture_mesh"
import "./outliner/locator"
import "./outliner/null_object"

View File

@ -1102,7 +1102,7 @@ BARS.defineActions(function() {
return Outliner.selected[0].scale[0]
} else if (Outliner.selected[0].size instanceof Array) {
return Outliner.selected[0].size[0];
} else if (Outliner.selected[0].resizable) {
} else if (Outliner.selected[0].getTypeBehavior('resizable')) {
return Outliner.selected[0].getSize(0, true);
}
},
@ -1129,7 +1129,7 @@ BARS.defineActions(function() {
return Outliner.selected[0].scale[1]
} else if (Outliner.selected[0].size instanceof Array) {
return Outliner.selected[0].size[1];
} else if (Outliner.selected[0].resizable) {
} else if (Outliner.selected[0].getTypeBehavior('resizable')) {
return Outliner.selected[0].getSize(1, true);
}
},
@ -1156,7 +1156,7 @@ BARS.defineActions(function() {
return Outliner.selected[0].scale[2]
} else if (Outliner.selected[0].size instanceof Array) {
return Outliner.selected[0].size[2];
} else if (Outliner.selected[0].resizable) {
} else if (Outliner.selected[0].getTypeBehavior('resizable')) {
return Outliner.selected[0].getSize(2, true);
}
},

View File

@ -1145,10 +1145,10 @@
obj.oldVertices[key] = obj.vertices[key].slice();
}
} else if (obj.getTypeBehavior('resizable')) {
obj.old_size = obj.size(axisnr)
obj.oldStretch = obj.stretch.slice();
obj.old_size = typeof obj.size == 'function' ? obj.size(axisnr) : obj.size.slice();
if (obj.stretch) obj.oldStretch = obj.stretch.slice();
if (obj.uv_offset) obj.oldUVOffset = obj.uv_offset.slice();
obj.oldCenter = obj.from.map((from, i) => (from + obj.to[i]) / 2);
if (obj.to && obj.to) obj.oldCenter = obj.from.map((from, i) => (from + obj.to[i]) / 2);
} else if (obj.size) {
obj.old_size = obj.size.slice();
}

View File

@ -1,9 +1,11 @@
import { THREE } from "../../lib/libs";
class BillboardFace extends CubeFace {
constructor(data, billboard) {
super();
this.texture = false;
this.billboard = billboard;
this.uv = [0, 0, canvasGridSize(), canvasGridSize()]
this.uv = [0, 0, canvasGridSize()*2, canvasGridSize()*2]
this.rotation = 0;
if (data) {
@ -39,7 +41,6 @@ class BillboardFace extends CubeFace {
return getRectangle(...this.uv);
}
}
BillboardFace.prototype.is_rectangular = true;
new Property(BillboardFace, 'number', 'rotation', {default: 0});
@ -59,7 +60,6 @@ class Billboard extends OutlinerElement {
Billboard.properties[key].reset(this);
}
this.box_uv = Project.box_uv;
this.faces = {
front: new BillboardFace(null, this),
}
@ -228,14 +228,19 @@ class Billboard extends OutlinerElement {
mapAutoUV() {
}
static behavior = {
select_faces: false,
cube_faces: true,
//rotatable: true,
movable: true,
resizable: true,
unique_name: false
}
}
Billboard.prototype.title = tl('data.billboard');
Billboard.prototype.type = 'billboard';
Billboard.prototype.icon = 'fa fas fa-bookmark';
Billboard.prototype.movable = true;
Billboard.prototype.resizable = true;
Billboard.prototype.rotatable = false;
Billboard.prototype.needsUniqueName = false;
Billboard.prototype.menu = new Menu([
...Outliner.control_menu_group,
'_',
@ -286,14 +291,35 @@ class Billboard extends OutlinerElement {
];
new Property(Billboard, 'string', 'name', {default: 'billboard'});
new Property(Billboard, 'vector', 'position');
new Property(Billboard, 'boolean', 'visibility', {default: true});
new Property(Billboard, 'boolean', 'locked');
new Property(Billboard, 'enum', 'facing_mode', {
default: 'lookat',
values: ['lookat', 'lookat_y', 'rotate', 'rotate_y'],
inputs: {
element_panel: {
input: {label: 'Facing Mode', type: 'select', options: {
lookat: 'Look At',
lookat_y: 'Look At Y',
rotate: 'Rotate',
rotate_y: 'Rotate Y',
}},
onChange() {
for (let billboard of Billboard.selected) {
Billboard.preview_controller.updateFacingCamera(billboard);
}
}
}
}
});
OutlinerElement.registerType(Billboard, 'billboard');
new NodePreviewController(Billboard, {
setup(element) {
let geometry = new THREE.PlaneBufferGeometry(2, 2);
let geometry = new THREE.PlaneGeometry(2, 2);
let mesh = new THREE.Mesh(geometry, Canvas.emptyMaterials[0]);
Project.nodes_3d[element.uuid] = mesh;
mesh.name = element.uuid;
@ -332,30 +358,28 @@ new NodePreviewController(Billboard, {
this.dispatchEvent('update_transform', {element});
},
updateGeometry(element) {
if (element.resizable) {
let mesh = element.mesh;
let half_size = [element.size[0]/2, element.size[1]/2];
let mesh = element.mesh;
let half_size = [element.size[0]/2, element.size[1]/2];
let corners = [
-half_size[0], half_size[1], 0,
half_size[0], half_size[1], 0,
-half_size[0], -half_size[1], 0,
half_size[0], -half_size[1], 0,
];
mesh.geometry.attributes.position.array.set(corners, 0);
mesh.geometry.attributes.position.needsUpdate = true;
mesh.geometry.computeBoundingBox()
mesh.geometry.computeBoundingSphere()
let corners = [
-half_size[0], half_size[1], 0,
half_size[0], half_size[1], 0,
-half_size[0], -half_size[1], 0,
half_size[0], -half_size[1], 0,
];
mesh.geometry.attributes.position.array.set(corners, 0);
mesh.geometry.attributes.position.needsUpdate = true;
mesh.geometry.computeBoundingBox()
mesh.geometry.computeBoundingSphere()
let outline_corners = [
-half_size[0], half_size[1], 0,
half_size[0], half_size[1], 0,
half_size[0], -half_size[1], 0,
-half_size[0], -half_size[1], 0,
-half_size[0], half_size[1], 0,
];
mesh.outline.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(outline_corners), 3));
}
let outline_corners = [
-half_size[0], half_size[1], 0,
half_size[0], half_size[1], 0,
half_size[0], -half_size[1], 0,
-half_size[0], -half_size[1], 0,
-half_size[0], half_size[1], 0,
];
mesh.outline.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(outline_corners), 3));
this.dispatchEvent('update_geometry', {element});
},
@ -363,7 +387,10 @@ new NodePreviewController(Billboard, {
let {mesh} = element;
if (Project.view_mode === 'solid') {
mesh.material = Canvas.solidMaterial
mesh.material = Canvas.monochromaticSolidMaterial
} else if (Project.view_mode === 'colored_solid') {
mesh.material = Canvas.coloredSolidMaterials[element.color % Canvas.emptyMaterials.length]
} else if (Project.view_mode === 'wireframe') {
mesh.material = Canvas.wireframeMaterial
@ -379,12 +406,12 @@ new NodePreviewController(Billboard, {
} else if (Format.single_texture) {
let tex = Texture.getDefault();
mesh.material = tex ? tex.getMaterial() : Canvas.emptyMaterials[element.color];
mesh.material = tex ? tex.getMaterial() : Canvas.emptyMaterials[element.color % Canvas.emptyMaterials.length];
} else {
let tex = element.faces.front.getTexture();
if (tex && tex.uuid) {
mesh.material = Project.materials[tex.uuid];
mesh.material = tex.getMaterial();
} else {
mesh.material = Canvas.emptyMaterials[element.color];
}
@ -397,12 +424,8 @@ new NodePreviewController(Billboard, {
let mesh = element.mesh
if (mesh === undefined || !mesh.geometry) return;
let stretch = 1
let frame = 0
stretch = 1;
frame = 0;
let stretch = 1;
let frame = 0;
let tex = element.faces.front.getTexture();
if (tex instanceof Texture && tex.frameCount !== 1) {
stretch = tex.frameCount
@ -430,19 +453,6 @@ new NodePreviewController(Billboard, {
}
stretch *= -1;
// Box UV fight texture bleeding
if (element.box_uv) {
uv = uv.slice();
for (let si = 0; si < 2; si++) {
let margin = 1/64;
if (uv[si] > uv[si+2]) {
margin = -margin
}
uv[si] += margin
uv[si+2] -= margin
}
}
let arr = [
[uv[0]/pw, (uv[1]/ph)/stretch+1],
[uv[2]/pw, (uv[1]/ph)/stretch+1],
@ -479,7 +489,37 @@ new NodePreviewController(Billboard, {
},
updateFacingCamera(element) {
//let scale = Preview.selected.calculateControlScale(billboard.getWorldPosition());
element.mesh.lookAt(Preview.selected.camera.position);
let {mesh} = element;
let vec = Reusable.vec1;
let dummy_vec = Reusable.vec2;
let world_quat_inverse = mesh.parent.getWorldQuaternion(Reusable.quat1).invert();
let camera = Preview.selected.camera;
switch (element.facing_mode) {
case 'lookat': {
mesh.lookAt(camera.position);
break;
}
case 'lookat_y': {
var v = vec.copy(camera.position);
dummy_vec.set(0, 0, 0);
mesh.localToWorld(dummy_vec);
v.y = dummy_vec.y;
mesh.lookAt(v);
break;
}
case 'rotate': {
mesh.rotation.copy(camera.rotation);
mesh.quaternion.premultiply(world_quat_inverse);
break;
}
case 'rotate_y': {
mesh.rotation.copy(camera.rotation);
mesh.rotation.reorder('YXZ');
mesh.rotation.x = mesh.rotation.z = 0;
mesh.quaternion.premultiply(world_quat_inverse);
break;
}
}
},
updateHighlight(element, hover_cube, force_off) {
let mesh = element.mesh;
@ -591,7 +631,7 @@ new NodePreviewController(Billboard, {
}
let geometry = new THREE.BufferGeometry();
let geometry = new THREE.Geometry();
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
let box = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({color: gizmo_colors.grid}));
@ -605,6 +645,34 @@ new NodePreviewController(Billboard, {
mesh.add(box);
this.dispatchEvent('update_painting_grid', {element: cube});
},
viewportRectangleOverlap(element, {projectPoint, rect_start, rect_end, preview}) {
if (BarItems.selection_mode.value != 'object' && Format.meshes && preview.selection.old_selected.find(el => el instanceof Mesh)) return;
let {mesh} = element;
let vector = Reusable.vec2;
let vertices = [
[element.size[0]/2, -element.size[1]/2, 0],
[element.size[0]/2, element.size[1]/2, 0],
[-element.size[0]/2, -element.size[1]/2, 0],
[-element.size[0]/2, element.size[1]/2, 0],
].map(coords => {
//coords.V3_subtract(element.origin);
vector.fromArray(coords);
mesh.localToWorld(vector);
return projectPoint(vector);
})
let is_on_screen = vertices.find(vertex => {
return (vertex[0] >= 0 && vertex[0] <= preview.width
&& vertex[1] >= 0 && vertex[1] <= preview.height);
})
return is_on_screen && (
lineIntersectsReactangle(vertices[0], vertices[1], rect_start, rect_end)
|| lineIntersectsReactangle(vertices[1], vertices[2], rect_start, rect_end)
|| lineIntersectsReactangle(vertices[2], vertices[3], rect_start, rect_end)
|| lineIntersectsReactangle(vertices[3], vertices[0], rect_start, rect_end)
);
}
})
@ -650,4 +718,9 @@ BARS.defineActions(function() {
return new_billboard
}
})
})
})
Object.assign(window, {
Billboard,
BillboardFace
})

View File

@ -942,7 +942,6 @@ export class Cube extends OutlinerElement {
}
static behavior = {
uv: 'cube',
select_faces: 'enum',
cube_faces: true,
rotatable: true,

View File

@ -426,6 +426,9 @@ export const UVEditor = {
},
getSelectedFaces(element, create) {
if (!element) return [];
if (element.getTypeBehavior('select_faces') == false) {
return Object.keys(element.faces);
}
if (element.getSelectedFaces) {
return element.getSelectedFaces(create);
} else {
@ -4138,6 +4141,7 @@ Interface.definePanels(function() {
},
isFaceSelected(element, fkey) {
if (!element) element = this.mappable_elements[0];
if (element.getTypeBehavior('select_faces') == false) return true;
return UVEditor.getSelectedFaces(element).indexOf(fkey) != -1;
},
isTransformingLayer() {
@ -4260,7 +4264,7 @@ Interface.definePanels(function() {
</div>
</div>
<div class="bar" id="uv_cube_face_bar" ref="uv_cube_face_bar" v-if="mode == 'uv' && mappable_elements[0] && mappable_elements[0].type != 'mesh' && !box_uv">
<div class="bar" id="uv_cube_face_bar" ref="uv_cube_face_bar" v-if="mode == 'uv' && mappable_elements[0] && mappable_elements[0].getTypeBehavior('select_faces') && !box_uv">
<li v-for="(face, key) in mappable_elements[0].faces" :face="key"
:class="{selected: isFaceSelected(null, key), disabled: mappable_elements[0].faces[key].texture === null}"
@mousedown="selectFace(null, key, $event, false, true)"
@ -4381,7 +4385,7 @@ Interface.definePanels(function() {
<template v-if="element.getTypeBehavior('cube_faces') && !element.box_uv">
<div class="cube_uv_face uv_face"
v-for="(face, key) in element.faces" :key="element.uuid + ':' + key"
v-if="(face.getTexture() == texture || texture == 0) && face.texture !== null && (display_uv !== 'selected_faces' || isFaceSelected(element, key))"
v-if="(face.getTexture() == texture || texture == 0) && face.texture !== null && (display_uv !== 'selected_faces' || isFaceSelected(element, key) || element.getTypeBehavior('select_faces') == false)"
:title="face_names[key]"
:class="{selected: isFaceSelected(element, key), unselected: display_uv === 'all_elements' && !mappable_elements.includes(element)}"
@mousedown.prevent="dragFace(element, key, $event)"