Advance Blender WebEngine N-015 through N-022 parity

This commit is contained in:
mes123456
2026-08-12 13:26:34 -04:00
parent 9fd26010f6
commit b3cefaeec5
51 changed files with 1809 additions and 171 deletions

View File

@@ -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;
}

View File

@@ -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;
});
}

View File

@@ -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 }

View File

@@ -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";

View 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;
}

View File

@@ -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 {