Skip to content

Commit

Permalink
feat: webgl 3d put hover, select, delete
Browse files Browse the repository at this point in the history
  • Loading branch information
plantain-00 committed Jun 21, 2024
1 parent 35535b4 commit 3963e8a
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 17 deletions.
137 changes: 125 additions & 12 deletions dev/webgl-3d-put.story.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLCanvasElement | null>(null)
Expand All @@ -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<number>()
const [selected, setSelected] = React.useState<number>()

React.useEffect(() => {
if (!ref.current || renderer.current) {
Expand All @@ -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<Graphic3d>[]) => {
Expand All @@ -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,
Expand All @@ -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 (
<div
Expand All @@ -89,7 +137,10 @@ export default () => {
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) {
Expand All @@ -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({
Expand All @@ -118,7 +169,12 @@ export default () => {
}
}}
onMouseDown={() => {
if (!status) return
if (!status) {
if (hovering) {
setSelected(hovering)
}
return
}
if (preview) {
setState(draft => {
draft.push(preview)
Expand All @@ -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(
<Menu
items={[
...items,
{
title: (
<>
<NumberEditor
value={colorRef.current}
value={selected ? vecToColorNumber(state[selected].color) : colorRef.current}
type='color'
style={{ width: '50px' }}
setValue={v => 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
}}
/>
</>
),
Expand All @@ -153,9 +239,17 @@ export default () => {
title: (
<>
<NumberEditor
value={opacityRef.current * 100}
value={(selected ? state[selected].color[3] : opacityRef.current) * 100}
style={{ width: '50px' }}
setValue={v => 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
}}
/>
</>
),
Expand All @@ -164,9 +258,28 @@ export default () => {
{
title: <>
<NumberEditor
value={sizeRef.current}
value={size ?? sizeRef.current}
style={{ width: '40px' }}
setValue={v => 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,
Expand Down
4 changes: 2 additions & 2 deletions spec/color.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>, color: number) {
t.deepEqual(recToColorNumber(colorNumberToVec(color)), color)
t.deepEqual(vecToColorNumber(colorNumberToVec(color)), color)
t.deepEqual(pixelColorToColorNumber(colorNumberToPixelColor(color)), color)
}

Expand Down
24 changes: 22 additions & 2 deletions src/components/webgpu-3d-renderer.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -357,6 +358,7 @@ export async function createWebgpu3DRenderer(canvas: HTMLCanvasElement) {
positionBuffer,
primaryBuffers,
count,
reversedProjection: m4.inverse(projection),
})
})

Expand Down Expand Up @@ -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
},
}
}

Expand All @@ -478,6 +497,7 @@ interface PickingObjectInfo {
texcoordBuffer: GPUBuffer;
indicesBuffer: GPUBuffer;
}
reversedProjection: m4.Mat4
}

function createVertexBuffer(device: GPUDevice, data: twgl.primitives.TypedArray) {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit 3963e8a

Please sign in to comment.