From 3963e8a4abc7c4056964cb00a22fba01ad5ddc31 Mon Sep 17 00:00:00 2001 From: york Date: Fri, 21 Jun 2024 08:02:03 +0800 Subject: [PATCH] feat: webgl 3d put hover, select, delete --- dev/webgl-3d-put.story.tsx | 137 +++++++++++++++++++++++--- spec/color.ts | 4 +- src/components/webgpu-3d-renderer.tsx | 24 ++++- src/utils/color.ts | 2 +- 4 files changed, 150 insertions(+), 17 deletions(-) diff --git a/dev/webgl-3d-put.story.tsx b/dev/webgl-3d-put.story.tsx index bc2a031d..63e1df88 100644 --- a/dev/webgl-3d-put.story.tsx +++ b/dev/webgl-3d-put.story.tsx @@ -1,5 +1,5 @@ import * as React from "react" -import { createWebgl3DRenderer, Graphic3d, useWindowSize, angleToRadian, Vec3, useGlobalKeyDown, Nullable, useUndoRedo, metaKeyIfMacElseCtrlKey, Menu, colorNumberToVec, NumberEditor } from "../src" +import { createWebgl3DRenderer, Graphic3d, useWindowSize, angleToRadian, Vec3, useGlobalKeyDown, Nullable, useUndoRedo, metaKeyIfMacElseCtrlKey, Menu, colorNumberToVec, NumberEditor, arcToPolyline, circleToArc, MenuItem, vecToColorNumber } from "../src" export default () => { const ref = React.useRef(null) @@ -24,6 +24,8 @@ export default () => { const colorRef = React.useRef(0xff0000) const opacityRef = React.useRef(1) const sizeRef = React.useRef(5) + const [hovering, setHovering] = React.useState() + const [selected, setSelected] = React.useState() React.useEffect(() => { if (!ref.current || renderer.current) { @@ -45,6 +47,15 @@ export default () => { setStatus(undefined) setContextMenu(undefined) setPreview(undefined) + setHovering(undefined) + setSelected(undefined) + } else if (e.key === 'Delete' || e.key === 'Backspace') { + if (selected) { + setState(draft => { + draft.splice(selected, 1) + }) + setSelected(undefined) + } } }) const render = (g: Nullable[]) => { @@ -59,7 +70,7 @@ export default () => { far: 1000, }, { - position: [1000, -1000, 1000], + position: [1000, -500, 800], color: [1, 1, 1, 1], specular: [1, 1, 1, 1], shininess: 50, @@ -74,8 +85,45 @@ export default () => { if (preview) { graphics.push(preview) } + const items = new Set([hovering, selected]) + for (const item of items) { + if (!item) continue + const g = state[item] + if (!g) continue + if (g.geometry.type === 'sphere') { + const points = arcToPolyline(circleToArc({ x: 0, y: 0, r: g.geometry.radius }), 5) + const result: number[] = [] + const z = 1 - g.geometry.radius + for (let i = 1; i < points.length; i++) { + result.push(points[i - 1].x, points[i - 1].y, z, points[i].x, points[i].y, z) + } + graphics.push({ + geometry: { + type: 'lines', + points: result, + }, + color: [0, 1, 0, 1], + position: g.position, + }) + } else if (g.geometry.type === 'cube') { + const points = arcToPolyline(circleToArc({ x: 0, y: 0, r: g.geometry.size * Math.SQRT1_2 }), 5) + const result: number[] = [] + const z = 1 - g.geometry.size / 2 + for (let i = 1; i < points.length; i++) { + result.push(points[i - 1].x, points[i - 1].y, z, points[i].x, points[i].y, z) + } + graphics.push({ + geometry: { + type: 'lines', + points: result, + }, + color: [0, 1, 0, 1], + position: g.position, + }) + } + } render(graphics) - }, [state, preview]) + }, [state, preview, hovering, selected]) return (
{ width={width} height={height} onMouseMove={e => { - if (!status) return + if (!status) { + setHovering(renderer.current?.pick(e.clientX, e.clientY)) + return + } if (renderer.current) { const info = renderer.current.pickingDrawObjectsInfo[0] if (info) { @@ -102,7 +153,7 @@ export default () => { size: sizeRef.current, }, color, - position: [target[0], target[1], sizeRef.current], + position: [target[0], target[1], sizeRef.current / 2], }) } else if (status === 'sphere') { setPreview({ @@ -118,7 +169,12 @@ export default () => { } }} onMouseDown={() => { - if (!status) return + if (!status) { + if (hovering) { + setSelected(hovering) + } + return + } if (preview) { setState(draft => { draft.push(preview) @@ -133,17 +189,47 @@ export default () => { setContextMenu(undefined) return } + const items: MenuItem[] = [] + let size: number | undefined + if (selected) { + items.push({ + title: 'delete', + onClick() { + setState(draft => { + draft.splice(selected, 1) + }) + setSelected(undefined) + setContextMenu(undefined) + }, + }) + const geometry = state[selected].geometry + if (geometry.type === 'cube') { + size = geometry.size + } else if (geometry.type === 'sphere') { + size = geometry.radius + } + } setContextMenu( colorRef.current = v} + setValue={v => { + if (selected) { + const color = state[selected].color + setState(draft => { + draft[selected].color = colorNumberToVec(v, color[3]) + }) + setContextMenu(undefined) + } + colorRef.current = v + }} /> ), @@ -153,9 +239,17 @@ export default () => { title: ( <> opacityRef.current = v * 0.01} + setValue={v => { + if (selected) { + setState(draft => { + draft[selected].color[3] = v * 0.01 + }) + setContextMenu(undefined) + } + opacityRef.current = v * 0.01 + }} /> ), @@ -164,9 +258,28 @@ export default () => { { title: <> sizeRef.current = v} + setValue={v => { + if (selected) { + setState(draft => { + const graphic = draft[selected] + if (graphic.geometry.type === 'cube') { + graphic.geometry.size = v + if (graphic.position) { + graphic.position[2] = v / 2 + } + } else if (graphic.geometry.type === 'sphere') { + graphic.geometry.radius = v + if (graphic.position) { + graphic.position[2] = v + } + } + }) + setContextMenu(undefined) + } + sizeRef.current = v + }} /> , height: 41, diff --git a/spec/color.ts b/spec/color.ts index 5e51e3ae..1e0839c4 100644 --- a/spec/color.ts +++ b/spec/color.ts @@ -1,8 +1,8 @@ import test, { ExecutionContext } from 'ava' -import { colorNumberToPixelColor, colorNumberToVec, pixelColorToColorNumber, recToColorNumber } from '../src' +import { colorNumberToPixelColor, colorNumberToVec, pixelColorToColorNumber, vecToColorNumber } from '../src' function check(t: ExecutionContext, color: number) { - t.deepEqual(recToColorNumber(colorNumberToVec(color)), color) + t.deepEqual(vecToColorNumber(colorNumberToVec(color)), color) t.deepEqual(pixelColorToColorNumber(colorNumberToPixelColor(color)), color) } diff --git a/src/components/webgpu-3d-renderer.tsx b/src/components/webgpu-3d-renderer.tsx index c213cdd6..3a327e90 100644 --- a/src/components/webgpu-3d-renderer.tsx +++ b/src/components/webgpu-3d-renderer.tsx @@ -1,7 +1,7 @@ import { m4 } from 'twgl.js' import * as twgl from 'twgl.js' import { MapCache, WeakmapCache } from '../utils/weakmap-cache' -import { Nullable, OptionalField, Vec4 } from '../utils/types' +import { Nullable, OptionalField, Vec3, Vec4 } from '../utils/types' import { Camera, Graphic3d, Light, get3dPolygonTriangles } from './webgl-3d-renderer' import { Lazy } from '../utils/lazy' import { createUniformsBuffer } from './react-render-target' @@ -286,10 +286,11 @@ export async function createWebgpu3DRenderer(canvas: HTMLCanvasElement) { }) }) passEncoder.setPipeline(pipeline); + const projection = m4.multiply(viewProjection, world) const bindGroupEntries: GPUBindGroupEntry[] = [{ binding: 0, resource: { buffer: createUniformsBuffer(device, [ - { type: 'mat4x4', value: m4.multiply(viewProjection, world) }, + { type: 'mat4x4', value: projection }, { type: 'vec3', value: light.position }, { type: 'mat4x4', value: world }, { type: 'mat4x4', value: camera }, @@ -357,6 +358,7 @@ export async function createWebgpu3DRenderer(canvas: HTMLCanvasElement) { positionBuffer, primaryBuffers, count, + reversedProjection: m4.inverse(projection), }) }) @@ -461,9 +463,26 @@ export async function createWebgpu3DRenderer(canvas: HTMLCanvasElement) { return index === 0xffffff ? undefined : index } + const getTarget = (inputX: number, inputY: number, eye: Vec3, z: number, reversedProjection: m4.Mat4): Vec3 => { + const rect = canvas.getBoundingClientRect() + const x = (inputX - rect.left) / canvas.clientWidth * 2 - 1 + const y = -((inputY - rect.top) / canvas.clientHeight * 2 - 1) + const a = m4.transformPoint(reversedProjection, [x, y, 1]) + const b = (z - eye[2]) / (a[2] - eye[2]) + return [ + eye[0] + (a[0] - eye[0]) * b, + eye[1] + (a[1] - eye[1]) * b, + z, + ] + } + return { render, pick, + getTarget, + get pickingDrawObjectsInfo() { + return pickingDrawObjectsInfo + }, } } @@ -478,6 +497,7 @@ interface PickingObjectInfo { texcoordBuffer: GPUBuffer; indicesBuffer: GPUBuffer; } + reversedProjection: m4.Mat4 } function createVertexBuffer(device: GPUDevice, data: twgl.primitives.TypedArray) { diff --git a/src/utils/color.ts b/src/utils/color.ts index d8af1e6f..24bfd78b 100644 --- a/src/utils/color.ts +++ b/src/utils/color.ts @@ -25,7 +25,7 @@ export function colorNumberToVec(n: number, alpha = 1): Vec4 { return [r / 255, g / 255, b / 255, alpha] } -export function recToColorNumber(color: Vec4) { +export function vecToColorNumber(color: Vec4) { return pixelColorToColorNumber([ Math.round(color[0] * 255), Math.round(color[1] * 255),