Advance Blender WebEngine N-015 through N-022 parity
This commit is contained in:
@@ -7,9 +7,14 @@ import {
|
||||
LineBasicMaterial,
|
||||
type Object3D,
|
||||
} from "../vendor/three/three.module.js";
|
||||
import type { GreasePencilDataIR, GreasePencilFrameIR } from "../../../protocol/grease-pencil";
|
||||
import type { GreasePencilDataIR, GreasePencilDrawingIR, GreasePencilFrameIR, GreasePencilLayerIR } from "../../../protocol/grease-pencil";
|
||||
import type { SceneNodeIR } from "../../../protocol/scene-ir";
|
||||
|
||||
interface DrawingPreview {
|
||||
drawing: GreasePencilDrawingIR;
|
||||
onion: "NONE" | "PREVIOUS" | "NEXT";
|
||||
}
|
||||
|
||||
function activeFrame(frames: readonly GreasePencilFrameIR[], frame: number): GreasePencilFrameIR | undefined {
|
||||
let selected: GreasePencilFrameIR | undefined;
|
||||
for (const candidate of frames) {
|
||||
@@ -18,45 +23,75 @@ function activeFrame(frames: readonly GreasePencilFrameIR[], frame: number): Gre
|
||||
return selected;
|
||||
}
|
||||
|
||||
function layerDrawings(layer: GreasePencilLayerIR, frame: number): DrawingPreview[] {
|
||||
const current = activeFrame(layer.frames, frame);
|
||||
if (!current) return [];
|
||||
const result: DrawingPreview[] = [{ drawing: current.drawing, onion: "NONE" }];
|
||||
if (!layer.onionSkinning) return result;
|
||||
const sorted = [...layer.frames].sort((left, right) => left.frame - right.frame);
|
||||
const currentIndex = sorted.findIndex((candidate) => candidate.frame === current.frame);
|
||||
if (currentIndex > 0) result.unshift({ drawing: sorted[currentIndex - 1].drawing, onion: "PREVIOUS" });
|
||||
if (currentIndex >= 0 && currentIndex + 1 < sorted.length) result.push({ drawing: sorted[currentIndex + 1].drawing, onion: "NEXT" });
|
||||
return result;
|
||||
}
|
||||
|
||||
function addDrawing(group: Group, layer: GreasePencilLayerIR, preview: DrawingPreview): number {
|
||||
let count = 0;
|
||||
for (const stroke of preview.drawing.strokes) {
|
||||
if (!stroke.points || stroke.points.length < 2) continue;
|
||||
const pointCount = stroke.points.length + (stroke.cyclic ? 1 : 0);
|
||||
const positions = new Float32Array(pointCount * 3);
|
||||
let red = 0;
|
||||
let green = 0;
|
||||
let blue = 0;
|
||||
let opacity = 0;
|
||||
for (let index = 0; index < pointCount; index++) {
|
||||
const point = stroke.points[index % stroke.points.length];
|
||||
positions[index * 3] = point.position[0];
|
||||
positions[index * 3 + 1] = point.position[2];
|
||||
positions[index * 3 + 2] = -point.position[1];
|
||||
}
|
||||
for (const point of stroke.points) {
|
||||
const color = point.vertexColor ?? [0.2, 0.2, 0.2, 1];
|
||||
red += color[0];
|
||||
green += color[1];
|
||||
blue += color[2];
|
||||
opacity += point.opacity * color[3];
|
||||
}
|
||||
const divisor = stroke.points.length;
|
||||
const geometry = new BufferGeometry();
|
||||
geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
|
||||
const onionColor = preview.onion === "PREVIOUS" ? new Color(0x6aa8ff) : preview.onion === "NEXT" ? new Color(0xff8a63) : null;
|
||||
const material = new LineBasicMaterial({
|
||||
color: onionColor ?? new Color(red / divisor, green / divisor, blue / divisor),
|
||||
opacity: Math.max(0, Math.min(1, layer.opacity * opacity / divisor * (preview.onion === "NONE" ? 1 : 0.28))),
|
||||
transparent: true,
|
||||
depthWrite: preview.onion === "NONE",
|
||||
});
|
||||
const line = new Line(geometry, material);
|
||||
line.userData.greasePencilOnion = preview.onion;
|
||||
line.userData.greasePencilMaterialIndex = stroke.materialIndex ?? 0;
|
||||
group.add(line);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export function createGreasePencilObject(data: GreasePencilDataIR, frame: number): Object3D | null {
|
||||
if (data.geometryStatus !== "available") return null;
|
||||
const group = new Group();
|
||||
let onionDrawingCount = 0;
|
||||
let currentDrawingCount = 0;
|
||||
for (const layer of data.layers) {
|
||||
if (!layer.visible || layer.opacity <= 0) continue;
|
||||
const drawing = activeFrame(layer.frames, frame)?.drawing;
|
||||
if (!drawing) continue;
|
||||
for (const stroke of drawing.strokes) {
|
||||
if (!stroke.points || stroke.points.length < 2) continue;
|
||||
const pointCount = stroke.points.length + (stroke.cyclic ? 1 : 0);
|
||||
const positions = new Float32Array(pointCount * 3);
|
||||
let red = 0;
|
||||
let green = 0;
|
||||
let blue = 0;
|
||||
let opacity = 0;
|
||||
for (let index = 0; index < pointCount; index++) {
|
||||
const point = stroke.points[index % stroke.points.length];
|
||||
positions[index * 3] = point.position[0];
|
||||
positions[index * 3 + 1] = point.position[2];
|
||||
positions[index * 3 + 2] = -point.position[1];
|
||||
}
|
||||
for (const point of stroke.points) {
|
||||
const color = point.vertexColor ?? [0.2, 0.2, 0.2, 1];
|
||||
red += color[0];
|
||||
green += color[1];
|
||||
blue += color[2];
|
||||
opacity += point.opacity * color[3];
|
||||
}
|
||||
const divisor = stroke.points.length;
|
||||
const geometry = new BufferGeometry();
|
||||
geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
|
||||
const material = new LineBasicMaterial({
|
||||
color: new Color(red / divisor, green / divisor, blue / divisor),
|
||||
opacity: Math.max(0, Math.min(1, layer.opacity * opacity / divisor)),
|
||||
transparent: true,
|
||||
});
|
||||
group.add(new Line(geometry, material));
|
||||
for (const preview of layerDrawings(layer, frame)) {
|
||||
const added = addDrawing(group, layer, preview);
|
||||
if (preview.onion === "NONE") currentDrawingCount += added;
|
||||
else onionDrawingCount += added;
|
||||
}
|
||||
}
|
||||
group.userData.greasePencilCurrentStrokeCount = currentDrawingCount;
|
||||
group.userData.greasePencilOnionStrokeCount = onionDrawingCount;
|
||||
return group.children.length > 0 ? group : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { NonMeshDataIR, SceneNodeIR } from "../../../protocol/scene-ir";
|
||||
import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary";
|
||||
|
||||
export type NonMeshElementKind = "CONTROL_POINT" | "HANDLE_LEFT" | "HANDLE_RIGHT";
|
||||
export type NonMeshElementSelection = ReadonlyMap<string, ReadonlyMap<NonMeshElementKind, ReadonlySet<number>>>;
|
||||
|
||||
function blenderPosition(x: number, y: number, z: number): [number, number, number] {
|
||||
return [x, z, -y];
|
||||
@@ -80,7 +81,7 @@ function createCurvePreview(data: NonMeshDataIR, points: ArrayLike<number> = dat
|
||||
controlPositions.set(blenderPosition(points[index], points[index + 1], points[index + 2]), index);
|
||||
}
|
||||
controlGeometry.setAttribute("position", new Float32BufferAttribute(controlPositions, 3));
|
||||
const controls = new Points(controlGeometry, new PointsMaterial({ color: new Color(0x67b7ff), size: 0.09, sizeAttenuation: true }));
|
||||
const controls = new Points(controlGeometry, new PointsMaterial({ color: new Color(0x67b7ff), size: 0.09, sizeAttenuation: true, vertexColors: true }));
|
||||
controls.userData.nonMeshDataId = data.id;
|
||||
controls.userData.nonMeshPointIndexMap = Array.from({ length: points.length / 3 }, (_, index) => index);
|
||||
controls.userData.nonMeshPointKindMap = Array.from({ length: points.length / 3 }, () => "CONTROL_POINT" as NonMeshElementKind);
|
||||
@@ -92,7 +93,7 @@ function createCurvePreview(data: NonMeshDataIR, points: ArrayLike<number> = dat
|
||||
group.add(lines);
|
||||
const pointGeometry = new BufferGeometry();
|
||||
pointGeometry.setAttribute("position", new Float32BufferAttribute(handlePositions, 3));
|
||||
const handles = new Points(pointGeometry, new PointsMaterial({ color: new Color(0xd7b8ff), size: 0.1, sizeAttenuation: true }));
|
||||
const handles = new Points(pointGeometry, new PointsMaterial({ color: new Color(0xd7b8ff), size: 0.1, sizeAttenuation: true, vertexColors: true }));
|
||||
handles.userData.nonMeshDataId = data.id;
|
||||
handles.userData.nonMeshPointIndexMap = handleIndexMap;
|
||||
handles.userData.nonMeshPointKindMap = handleKindMap;
|
||||
@@ -172,3 +173,25 @@ export function applyNonMeshTransform(object: Object3D, node: SceneNodeIR): void
|
||||
child.userData.blenderId = node.id;
|
||||
});
|
||||
}
|
||||
|
||||
export function applyNonMeshElementSelection(root: Object3D, selection: NonMeshElementSelection): void {
|
||||
root.traverse((object) => {
|
||||
const dataId = object.userData.nonMeshDataId;
|
||||
const indexMap = object.userData.nonMeshPointIndexMap as number[] | undefined;
|
||||
const kindMap = object.userData.nonMeshPointKindMap as NonMeshElementKind[] | undefined;
|
||||
if (typeof dataId !== "string" || !indexMap || !kindMap || !(object instanceof Points) || !(object.material instanceof PointsMaterial)) return;
|
||||
const selectedByKind = selection.get(dataId);
|
||||
const colors = new Float32Array(indexMap.length * 3);
|
||||
for (let index = 0; index < indexMap.length; index++) {
|
||||
const selected = selectedByKind?.get(kindMap[index])?.has(indexMap[index]) ?? false;
|
||||
const color = selected ? [1, 0.4, 0.1] : kindMap[index] === "CONTROL_POINT" ? [0.4, 0.72, 1] : [0.6, 0.48, 1];
|
||||
colors[index * 3] = color[0];
|
||||
colors[index * 3 + 1] = color[1];
|
||||
colors[index * 3 + 2] = color[2];
|
||||
}
|
||||
object.geometry.setAttribute("color", new Float32BufferAttribute(colors, 3));
|
||||
object.material.color.set(0xffffff);
|
||||
object.material.vertexColors = true;
|
||||
object.material.needsUpdate = true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export type OffscreenViewportRequest =
|
||||
| { type: "snapshot"; snapshot: SceneSnapshotIR; geometryBuffers: MeshGeometryBuffer[]; nonMeshGeometryBuffers: NonMeshGeometryChunk[] }
|
||||
| { type: "textureAssets"; assets: GPUTextureAsset[] }
|
||||
| { type: "resize"; width: number; height: number; pixelRatio: number }
|
||||
| { type: "selection"; objectIds: string[] }
|
||||
| { type: "selection"; objectIds: string[]; elements: Array<{ dataId: string; kind: NonMeshElementKind; index: number }> }
|
||||
| { type: "interaction"; editMode: boolean; selectionMode: MeshElementMode }
|
||||
| { type: "orbit"; deltaX: number; deltaY: number; zoom: number }
|
||||
| { type: "pick"; x: number; y: number; additive: boolean }
|
||||
@@ -18,7 +18,7 @@ export type OffscreenViewportRequest =
|
||||
export type OffscreenViewportResponse =
|
||||
| { type: "ready" }
|
||||
| { type: "frame"; visiblePixels: number }
|
||||
| { type: "snapshotStatus"; nonMeshCount: number; nonMeshBlockedCount: number; greasePencilCount: number; greasePencilBlockedCount: number }
|
||||
| { type: "snapshotStatus"; nonMeshCount: number; nonMeshBlockedCount: number; greasePencilCount: number; greasePencilBlockedCount: number; greasePencilOnionStrokeCount: number }
|
||||
| { type: "textureStatus"; loaded: number; rejected: number; bytes: number; errors: string[]; errorCodes: string[] }
|
||||
| { type: "selected"; objectId: string; additive: boolean }
|
||||
| { type: "elementSelected"; meshId: string; mode: MeshElementMode; index: number; additive: boolean; nonMeshKind?: NonMeshElementKind }
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { NonMeshElementKind } from "./nonmesh";
|
||||
export interface ViewportBackend {
|
||||
setSnapshot(snapshot: SceneSnapshotIR, geometryBuffers?: MeshGeometryBuffer[], nonMeshGeometryBuffers?: NonMeshGeometryChunk[]): void;
|
||||
setTextureAssets(assets: readonly GPUTextureAsset[]): void;
|
||||
setSelection(objectIds: ReadonlySet<string>): void;
|
||||
setSelection(objectIds: ReadonlySet<string>, elementSelection?: ReadonlyMap<string, ReadonlyMap<NonMeshElementKind, ReadonlySet<number>>>): void;
|
||||
setInteractionMode(editMode: boolean, selectionMode: MeshElementMode): void;
|
||||
installLODLevels(meshId: string, levels: readonly WebEngineLODLevelResult[]): void;
|
||||
dispose(): void;
|
||||
@@ -147,8 +147,9 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
this.worker.postMessage({ type: "textureAssets", assets: cloned } satisfies OffscreenViewportRequest, transfer);
|
||||
}
|
||||
|
||||
setSelection(objectIds: ReadonlySet<string>): void {
|
||||
this.worker.postMessage({ type: "selection", objectIds: [...objectIds] } satisfies OffscreenViewportRequest);
|
||||
setSelection(objectIds: ReadonlySet<string>, elementSelection?: ReadonlyMap<string, ReadonlyMap<NonMeshElementKind, ReadonlySet<number>>>): void {
|
||||
const elements = [...(elementSelection ?? new Map())].flatMap(([dataId, kinds]) => [...kinds].flatMap(([kind, indices]) => [...indices].map((index) => ({ dataId, kind, index }))));
|
||||
this.worker.postMessage({ type: "selection", objectIds: [...objectIds], elements } satisfies OffscreenViewportRequest);
|
||||
}
|
||||
|
||||
setInteractionMode(editMode: boolean, selectionMode: MeshElementMode): void {
|
||||
@@ -209,6 +210,7 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
this.canvas.dataset.nonMeshBlockedCount = String(message.nonMeshBlockedCount);
|
||||
this.canvas.dataset.greasePencilCount = String(message.greasePencilCount);
|
||||
this.canvas.dataset.greasePencilBlockedCount = String(message.greasePencilBlockedCount);
|
||||
this.canvas.dataset.greasePencilOnionStrokeCount = String(message.greasePencilOnionStrokeCount);
|
||||
}
|
||||
else if (message.type === "textureStatus") {
|
||||
this.canvas.dataset.textureStatus = message.rejected > 0 ? "blocked" : "ready";
|
||||
|
||||
62
web/app/src/three-adapter/paint-hit.ts
Normal file
62
web/app/src/three-adapter/paint-hit.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
Matrix3,
|
||||
Mesh,
|
||||
Triangle,
|
||||
Vector2,
|
||||
Vector3,
|
||||
type Intersection,
|
||||
} from "../vendor/three/three.module.js";
|
||||
import type { PaintHitIR } from "../../../protocol/paint";
|
||||
|
||||
export interface PaintRaycastHitIR extends PaintHitIR {
|
||||
objectId: string;
|
||||
dataId: string;
|
||||
}
|
||||
|
||||
function finiteTuple(values: readonly number[]): boolean {
|
||||
return values.every(Number.isFinite);
|
||||
}
|
||||
|
||||
export function paintHitFromIntersection(intersection: Intersection, pressure = 1): PaintRaycastHitIR | null {
|
||||
const faceIndex = intersection.faceIndex;
|
||||
if (!(intersection.object instanceof Mesh) || faceIndex === undefined || faceIndex === null || faceIndex < 0 || pressure < 0 || pressure > 1) return null;
|
||||
const object = intersection.object;
|
||||
const positionAttribute = object.geometry.getAttribute("position");
|
||||
const indexAttribute = object.geometry.getIndex();
|
||||
if (!positionAttribute) return null;
|
||||
const corner = faceIndex * 3;
|
||||
const vertexA = indexAttribute ? indexAttribute.getX(corner) : corner;
|
||||
const vertexB = indexAttribute ? indexAttribute.getX(corner + 1) : corner + 1;
|
||||
const vertexC = indexAttribute ? indexAttribute.getX(corner + 2) : corner + 2;
|
||||
if ([vertexA, vertexB, vertexC].some((vertex) => vertex < 0 || vertex >= positionAttribute.count)) return null;
|
||||
const a = new Vector3().fromBufferAttribute(positionAttribute, vertexA);
|
||||
const b = new Vector3().fromBufferAttribute(positionAttribute, vertexB);
|
||||
const c = new Vector3().fromBufferAttribute(positionAttribute, vertexC);
|
||||
const localPoint = object.worldToLocal(intersection.point.clone());
|
||||
const barycentric = Triangle.getBarycoord(localPoint, a, b, c, new Vector3());
|
||||
if (!barycentric || !finiteTuple(barycentric.toArray())) return null;
|
||||
const localNormal = intersection.face?.normal?.clone() ?? new Triangle(a, b, c).getNormal(new Vector3());
|
||||
const worldNormal = localNormal.applyNormalMatrix(new Matrix3().getNormalMatrix(object.matrixWorld)).normalize();
|
||||
const sourceFaces = object.userData.triangleFaceIndices as number[] | undefined;
|
||||
const result: PaintRaycastHitIR = {
|
||||
objectId: String(object.userData.blenderId ?? ""),
|
||||
dataId: String(object.userData.meshId ?? ""),
|
||||
position: intersection.point.toArray(),
|
||||
normal: worldNormal.toArray(),
|
||||
faceIndex: sourceFaces?.[faceIndex] ?? faceIndex,
|
||||
barycentric: barycentric.toArray(),
|
||||
pressure,
|
||||
};
|
||||
if (!result.objectId || !result.dataId) return null;
|
||||
const uv = object.geometry.getAttribute("uv");
|
||||
if (uv) {
|
||||
const uvA = new Vector2().fromBufferAttribute(uv, vertexA);
|
||||
const uvB = new Vector2().fromBufferAttribute(uv, vertexB);
|
||||
const uvC = new Vector2().fromBufferAttribute(uv, vertexC);
|
||||
result.uv = [
|
||||
uvA.x * barycentric.x + uvB.x * barycentric.y + uvC.x * barycentric.z,
|
||||
uvA.y * barycentric.x + uvB.y * barycentric.y + uvC.y * barycentric.z,
|
||||
];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
import { GPUTextureStore } from "./texture-assets";
|
||||
import type { GPUTextureAsset } from "../../../protocol/render-assets";
|
||||
import { gateEnvironmentImage, gateUDIMImage } from "../../../protocol/render-assets";
|
||||
import { applyNonMeshTransform, createNonMeshObject, type NonMeshElementKind } from "./nonmesh";
|
||||
import { applyNonMeshElementSelection, applyNonMeshTransform, createNonMeshObject, type NonMeshElementKind } from "./nonmesh";
|
||||
import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary";
|
||||
import { applyGreasePencilTransform, createGreasePencilObject } from "./grease-pencil";
|
||||
|
||||
@@ -279,6 +279,7 @@ export class ViewportRenderer {
|
||||
const dataById = new Map((snapshot.greasePencils ?? []).map((data) => [data.id, data]));
|
||||
let previewCount = 0;
|
||||
let blockedCount = 0;
|
||||
let onionStrokeCount = 0;
|
||||
for (const node of snapshot.nodes) {
|
||||
if (node.type !== "GREASE_PENCIL" || !node.visible || !node.dataId) continue;
|
||||
const data = dataById.get(node.dataId);
|
||||
@@ -291,10 +292,12 @@ export class ViewportRenderer {
|
||||
applyGreasePencilTransform(object, node);
|
||||
this.importedRoot.add(object);
|
||||
this.objectByBlenderId.set(node.id, object);
|
||||
onionStrokeCount += Number(object.userData.greasePencilOnionStrokeCount ?? 0);
|
||||
previewCount++;
|
||||
}
|
||||
this.canvas.dataset.greasePencilCount = String(previewCount);
|
||||
this.canvas.dataset.greasePencilBlockedCount = String(blockedCount);
|
||||
this.canvas.dataset.greasePencilOnionStrokeCount = String(onionStrokeCount);
|
||||
}
|
||||
|
||||
setTextureAssets(assets: readonly GPUTextureAsset[]): void {
|
||||
@@ -350,7 +353,7 @@ export class ViewportRenderer {
|
||||
this.currentSnapshot = next;
|
||||
}
|
||||
|
||||
setSelection(objectIds: ReadonlySet<string>): void {
|
||||
setSelection(objectIds: ReadonlySet<string>, elementSelection?: ReadonlyMap<string, ReadonlyMap<NonMeshElementKind, ReadonlySet<number>>>): void {
|
||||
const visitedInstances = new Set<InstancedMesh>();
|
||||
for (const [objectId, object] of this.objectByBlenderId) {
|
||||
if (object instanceof InstancedMesh) {
|
||||
@@ -370,6 +373,7 @@ export class ViewportRenderer {
|
||||
setPBRMaterialSelected(material, objectIds.has(objectId));
|
||||
}
|
||||
}
|
||||
applyNonMeshElementSelection(this.importedRoot, elementSelection ?? new Map());
|
||||
}
|
||||
|
||||
setInteractionMode(editMode: boolean, selectionMode: MeshElementMode): void {
|
||||
|
||||
Reference in New Issue
Block a user