2D/3D视口
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Play, Pause, RotateCcw, Maximize2, Grid3x3, Eye, EyeOff, Activity } from 'lucide-react';
|
import { Play, Pause, RotateCcw, Maximize2, Grid3x3, Eye, EyeOff, Activity, Box, Square } from 'lucide-react';
|
||||||
import '../styles/Viewport.css';
|
import '../styles/Viewport.css';
|
||||||
|
|
||||||
interface ViewportProps {
|
interface ViewportProps {
|
||||||
@@ -13,8 +13,23 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
|||||||
const [showGrid, setShowGrid] = useState(true);
|
const [showGrid, setShowGrid] = useState(true);
|
||||||
const [showGizmos, setShowGizmos] = useState(true);
|
const [showGizmos, setShowGizmos] = useState(true);
|
||||||
const [showStats, setShowStats] = useState(false);
|
const [showStats, setShowStats] = useState(false);
|
||||||
|
const [is3D, setIs3D] = useState(true);
|
||||||
const animationFrameRef = useRef<number>();
|
const animationFrameRef = useRef<number>();
|
||||||
const glRef = useRef<WebGLRenderingContext | null>(null);
|
const glRef = useRef<WebGLRenderingContext | null>(null);
|
||||||
|
const gridProgramRef = useRef<WebGLProgram | null>(null);
|
||||||
|
const gridBufferRef = useRef<WebGLBuffer | null>(null);
|
||||||
|
const dynamicGridBufferRef = useRef<WebGLBuffer | null>(null);
|
||||||
|
const axisBufferRef = useRef<WebGLBuffer | null>(null);
|
||||||
|
const [cameraRotation, setCameraRotation] = useState({ yaw: -Math.PI / 4, pitch: Math.PI / 6 });
|
||||||
|
const [cameraDistance, setCameraDistance] = useState(20);
|
||||||
|
const [camera2DOffset, setCamera2DOffset] = useState({ x: 0, y: 0 });
|
||||||
|
const [camera2DZoom, setCamera2DZoom] = useState(20);
|
||||||
|
const isDraggingRef = useRef(false);
|
||||||
|
const lastMousePosRef = useRef({ x: 0, y: 0 });
|
||||||
|
const [fps, setFps] = useState(0);
|
||||||
|
const [drawCalls, setDrawCalls] = useState(0);
|
||||||
|
const fpsFrameCountRef = useRef(0);
|
||||||
|
const fpsLastTimeRef = useRef(performance.now());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
@@ -45,60 +60,101 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
|||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
window.addEventListener('resize', resizeCanvas);
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
resizeCanvas();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (containerRef.current) {
|
||||||
|
resizeObserver.observe(containerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
initWebGL(gl);
|
initWebGL(gl);
|
||||||
|
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
if (e.button === 0) {
|
||||||
|
isDraggingRef.current = true;
|
||||||
|
lastMousePosRef.current = { x: e.clientX, y: e.clientY };
|
||||||
|
canvas.style.cursor = 'grabbing';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isDraggingRef.current) return;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - lastMousePosRef.current.x;
|
||||||
|
const deltaY = e.clientY - lastMousePosRef.current.y;
|
||||||
|
|
||||||
|
if (is3D) {
|
||||||
|
setCameraRotation(prev => ({
|
||||||
|
yaw: prev.yaw - deltaX * 0.005,
|
||||||
|
pitch: Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, prev.pitch + deltaY * 0.005))
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setCamera2DOffset(prev => ({
|
||||||
|
x: prev.x - deltaX * 0.05,
|
||||||
|
y: prev.y - deltaY * 0.05
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMousePosRef.current = { x: e.clientX, y: e.clientY };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
isDraggingRef.current = false;
|
||||||
|
if (canvas) {
|
||||||
|
canvas.style.cursor = 'grab';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (is3D) {
|
||||||
|
setCameraDistance(prev => Math.max(5, Math.min(50, prev + e.deltaY * 0.01)));
|
||||||
|
} else {
|
||||||
|
setCamera2DZoom(prev => Math.max(5, Math.min(100, prev + e.deltaY * 0.01)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas.addEventListener('mousedown', handleMouseDown);
|
||||||
|
canvas.addEventListener('mousemove', handleMouseMove);
|
||||||
|
canvas.addEventListener('mouseup', handleMouseUp);
|
||||||
|
canvas.addEventListener('mouseleave', handleMouseUp);
|
||||||
|
canvas.addEventListener('wheel', handleWheel, { passive: false });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', resizeCanvas);
|
window.removeEventListener('resize', resizeCanvas);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
canvas.removeEventListener('mousedown', handleMouseDown);
|
||||||
|
canvas.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
canvas.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
canvas.removeEventListener('mouseleave', handleMouseUp);
|
||||||
|
canvas.removeEventListener('wheel', handleWheel);
|
||||||
if (animationFrameRef.current) {
|
if (animationFrameRef.current) {
|
||||||
cancelAnimationFrame(animationFrameRef.current);
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [is3D]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPlaying) {
|
startRenderLoop();
|
||||||
startRenderLoop();
|
return () => {
|
||||||
} else {
|
|
||||||
if (animationFrameRef.current) {
|
if (animationFrameRef.current) {
|
||||||
cancelAnimationFrame(animationFrameRef.current);
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [isPlaying]);
|
}, [isPlaying, showGrid, cameraRotation, cameraDistance, camera2DOffset, camera2DZoom, is3D]);
|
||||||
|
|
||||||
const initWebGL = (gl: WebGLRenderingContext) => {
|
const initWebGL = (gl: WebGLRenderingContext) => {
|
||||||
gl.clearColor(0.1, 0.1, 0.12, 1.0);
|
gl.clearColor(0.1, 0.1, 0.12, 1.0);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
||||||
gl.enable(gl.DEPTH_TEST);
|
gl.enable(gl.DEPTH_TEST);
|
||||||
|
gl.enable(gl.BLEND);
|
||||||
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
initGridProgram(gl);
|
||||||
renderFrame(gl, 0);
|
renderFrame(gl, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startRenderLoop = () => {
|
const initGridProgram = (gl: WebGLRenderingContext) => {
|
||||||
let startTime = performance.now();
|
|
||||||
|
|
||||||
const render = (currentTime: number) => {
|
|
||||||
const elapsed = (currentTime - startTime) / 1000;
|
|
||||||
|
|
||||||
if (glRef.current) {
|
|
||||||
renderFrame(glRef.current, elapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
animationFrameRef.current = requestAnimationFrame(render);
|
|
||||||
};
|
|
||||||
|
|
||||||
animationFrameRef.current = requestAnimationFrame(render);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFrame = (gl: WebGLRenderingContext, time: number) => {
|
|
||||||
gl.clearColor(0.1, 0.1, 0.12, 1.0);
|
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
||||||
|
|
||||||
if (showGrid) {
|
|
||||||
drawGrid(gl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawGrid = (gl: WebGLRenderingContext) => {
|
|
||||||
const vertexShaderSource = `
|
const vertexShaderSource = `
|
||||||
attribute vec3 position;
|
attribute vec3 position;
|
||||||
uniform mat4 projection;
|
uniform mat4 projection;
|
||||||
@@ -120,17 +176,33 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
|||||||
gl.shaderSource(vertexShader, vertexShaderSource);
|
gl.shaderSource(vertexShader, vertexShaderSource);
|
||||||
gl.compileShader(vertexShader);
|
gl.compileShader(vertexShader);
|
||||||
|
|
||||||
|
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
||||||
|
console.error('Vertex shader compilation error:', gl.getShaderInfoLog(vertexShader));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
|
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
|
||||||
gl.shaderSource(fragmentShader, fragmentShaderSource);
|
gl.shaderSource(fragmentShader, fragmentShaderSource);
|
||||||
gl.compileShader(fragmentShader);
|
gl.compileShader(fragmentShader);
|
||||||
|
|
||||||
|
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
||||||
|
console.error('Fragment shader compilation error:', gl.getShaderInfoLog(fragmentShader));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const program = gl.createProgram()!;
|
const program = gl.createProgram()!;
|
||||||
gl.attachShader(program, vertexShader);
|
gl.attachShader(program, vertexShader);
|
||||||
gl.attachShader(program, fragmentShader);
|
gl.attachShader(program, fragmentShader);
|
||||||
gl.linkProgram(program);
|
gl.linkProgram(program);
|
||||||
gl.useProgram(program);
|
|
||||||
|
|
||||||
const gridSize = 10;
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||||
|
console.error('Program linking error:', gl.getProgramInfoLog(program));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gridProgramRef.current = program;
|
||||||
|
|
||||||
|
const gridSize = 100;
|
||||||
const gridStep = 1;
|
const gridStep = 1;
|
||||||
const vertices: number[] = [];
|
const vertices: number[] = [];
|
||||||
|
|
||||||
@@ -143,14 +215,356 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
|||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
gridBufferRef.current = buffer;
|
||||||
|
|
||||||
|
const axisLength = 5;
|
||||||
|
const axisVertices = [
|
||||||
|
0, 0, 0, axisLength, 0, 0,
|
||||||
|
0, 0, 0, 0, axisLength, 0,
|
||||||
|
0, 0, 0, 0, 0, axisLength
|
||||||
|
];
|
||||||
|
|
||||||
|
const axisBuffer = gl.createBuffer();
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, axisBuffer);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(axisVertices), gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
axisBufferRef.current = axisBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRenderLoop = () => {
|
||||||
|
let startTime = performance.now();
|
||||||
|
|
||||||
|
const render = (currentTime: number) => {
|
||||||
|
const elapsed = (currentTime - startTime) / 1000;
|
||||||
|
|
||||||
|
if (glRef.current) {
|
||||||
|
renderFrame(glRef.current, elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameRef.current = requestAnimationFrame(render);
|
||||||
|
};
|
||||||
|
|
||||||
|
animationFrameRef.current = requestAnimationFrame(render);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFrame = (gl: WebGLRenderingContext, time: number) => {
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
let currentDrawCalls = 0;
|
||||||
|
|
||||||
|
if (showGrid && gridProgramRef.current && gridBufferRef.current) {
|
||||||
|
drawGrid(gl);
|
||||||
|
currentDrawCalls += is3D ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.disable(gl.DEPTH_TEST);
|
||||||
|
if (gridProgramRef.current && axisBufferRef.current) {
|
||||||
|
drawAxis(gl);
|
||||||
|
currentDrawCalls += is3D ? 3 : 2;
|
||||||
|
}
|
||||||
|
gl.enable(gl.DEPTH_TEST);
|
||||||
|
|
||||||
|
setDrawCalls(currentDrawCalls);
|
||||||
|
|
||||||
|
fpsFrameCountRef.current++;
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const deltaTime = currentTime - fpsLastTimeRef.current;
|
||||||
|
|
||||||
|
if (deltaTime >= 1000) {
|
||||||
|
const currentFps = Math.round((fpsFrameCountRef.current * 1000) / deltaTime);
|
||||||
|
setFps(currentFps);
|
||||||
|
fpsFrameCountRef.current = 0;
|
||||||
|
fpsLastTimeRef.current = currentTime;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPerspectiveMatrix = (fov: number, aspect: number, near: number, far: number): Float32Array => {
|
||||||
|
const f = 1.0 / Math.tan(fov / 2);
|
||||||
|
const rangeInv = 1.0 / (near - far);
|
||||||
|
|
||||||
|
return new Float32Array([
|
||||||
|
f / aspect, 0, 0, 0,
|
||||||
|
0, f, 0, 0,
|
||||||
|
0, 0, (near + far) * rangeInv, -1,
|
||||||
|
0, 0, near * far * rangeInv * 2, 0
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createOrthographicMatrix = (left: number, right: number, bottom: number, top: number, near: number, far: number): Float32Array => {
|
||||||
|
const lr = 1 / (left - right);
|
||||||
|
const bt = 1 / (bottom - top);
|
||||||
|
const nf = 1 / (near - far);
|
||||||
|
|
||||||
|
return new Float32Array([
|
||||||
|
-2 * lr, 0, 0, 0,
|
||||||
|
0, -2 * bt, 0, 0,
|
||||||
|
0, 0, 2 * nf, 0,
|
||||||
|
(left + right) * lr, (top + bottom) * bt, (near + far) * nf, 1
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createLookAtMatrix = (
|
||||||
|
eyeX: number, eyeY: number, eyeZ: number,
|
||||||
|
centerX: number, centerY: number, centerZ: number,
|
||||||
|
upX: number, upY: number, upZ: number
|
||||||
|
): Float32Array => {
|
||||||
|
let zx = eyeX - centerX;
|
||||||
|
let zy = eyeY - centerY;
|
||||||
|
let zz = eyeZ - centerZ;
|
||||||
|
const zlen = Math.sqrt(zx * zx + zy * zy + zz * zz);
|
||||||
|
zx /= zlen;
|
||||||
|
zy /= zlen;
|
||||||
|
zz /= zlen;
|
||||||
|
|
||||||
|
let xx = upY * zz - upZ * zy;
|
||||||
|
let xy = upZ * zx - upX * zz;
|
||||||
|
let xz = upX * zy - upY * zx;
|
||||||
|
const xlen = Math.sqrt(xx * xx + xy * xy + xz * xz);
|
||||||
|
xx /= xlen;
|
||||||
|
xy /= xlen;
|
||||||
|
xz /= xlen;
|
||||||
|
|
||||||
|
const yx = zy * xz - zz * xy;
|
||||||
|
const yy = zz * xx - zx * xz;
|
||||||
|
const yz = zx * xy - zy * xx;
|
||||||
|
|
||||||
|
return new Float32Array([
|
||||||
|
xx, yx, zx, 0,
|
||||||
|
xy, yy, zy, 0,
|
||||||
|
xz, yz, zz, 0,
|
||||||
|
-(xx * eyeX + xy * eyeY + xz * eyeZ),
|
||||||
|
-(yx * eyeX + yy * eyeY + yz * eyeZ),
|
||||||
|
-(zx * eyeX + zy * eyeY + zz * eyeZ),
|
||||||
|
1
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDynamicGrid = (gl: WebGLRenderingContext, zoom: number, aspect: number) => {
|
||||||
|
const viewWidth = zoom * aspect * 2;
|
||||||
|
const viewHeight = zoom * 2;
|
||||||
|
const maxViewSize = Math.max(viewWidth, viewHeight);
|
||||||
|
|
||||||
|
let baseGridStep = 1;
|
||||||
|
if (maxViewSize > 200) {
|
||||||
|
baseGridStep = 100;
|
||||||
|
} else if (maxViewSize > 100) {
|
||||||
|
baseGridStep = 10;
|
||||||
|
} else if (maxViewSize > 50) {
|
||||||
|
baseGridStep = 10;
|
||||||
|
} else if (maxViewSize > 20) {
|
||||||
|
baseGridStep = 1;
|
||||||
|
} else if (maxViewSize > 10) {
|
||||||
|
baseGridStep = 1;
|
||||||
|
} else if (maxViewSize > 5) {
|
||||||
|
baseGridStep = 0.1;
|
||||||
|
} else {
|
||||||
|
baseGridStep = 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fineGridStep = baseGridStep;
|
||||||
|
const coarseGridStep = baseGridStep * 10;
|
||||||
|
|
||||||
|
const gridRange = Math.ceil(maxViewSize * 0.75);
|
||||||
|
const vertices: number[] = [];
|
||||||
|
const coarseVertices: number[] = [];
|
||||||
|
|
||||||
|
const startX = Math.floor((-viewWidth / 2 - gridRange) / fineGridStep) * fineGridStep;
|
||||||
|
const endX = Math.ceil((viewWidth / 2 + gridRange) / fineGridStep) * fineGridStep;
|
||||||
|
const startZ = Math.floor((-viewHeight / 2 - gridRange) / fineGridStep) * fineGridStep;
|
||||||
|
const endZ = Math.ceil((viewHeight / 2 + gridRange) / fineGridStep) * fineGridStep;
|
||||||
|
|
||||||
|
for (let x = startX; x <= endX; x += fineGridStep) {
|
||||||
|
const roundedX = Math.round(x / fineGridStep) * fineGridStep;
|
||||||
|
if (Math.abs(roundedX % coarseGridStep) < 0.001) {
|
||||||
|
coarseVertices.push(roundedX, 0, startZ, roundedX, 0, endZ);
|
||||||
|
} else {
|
||||||
|
vertices.push(roundedX, 0, startZ, roundedX, 0, endZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let z = startZ; z <= endZ; z += fineGridStep) {
|
||||||
|
const roundedZ = Math.round(z / fineGridStep) * fineGridStep;
|
||||||
|
if (Math.abs(roundedZ % coarseGridStep) < 0.001) {
|
||||||
|
coarseVertices.push(startX, 0, roundedZ, endX, 0, roundedZ);
|
||||||
|
} else {
|
||||||
|
vertices.push(startX, 0, roundedZ, endX, 0, roundedZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dynamicGridBufferRef.current) {
|
||||||
|
dynamicGridBufferRef.current = gl.createBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, dynamicGridBufferRef.current);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([...vertices, ...coarseVertices]), gl.DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fineLineCount: vertices.length / 6,
|
||||||
|
coarseLineCount: coarseVertices.length / 6,
|
||||||
|
totalLineCount: (vertices.length + coarseVertices.length) / 6
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawGrid = (gl: WebGLRenderingContext) => {
|
||||||
|
const program = gridProgramRef.current;
|
||||||
|
if (!program) return;
|
||||||
|
|
||||||
|
gl.useProgram(program);
|
||||||
|
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const aspect = canvas.width / canvas.height;
|
||||||
|
let projectionMatrix: Float32Array;
|
||||||
|
let viewMatrix: Float32Array;
|
||||||
|
|
||||||
|
const projectionLocation = gl.getUniformLocation(program, 'projection');
|
||||||
|
const viewLocation = gl.getUniformLocation(program, 'view');
|
||||||
|
const colorLocation = gl.getUniformLocation(program, 'color');
|
||||||
|
const positionLocation = gl.getAttribLocation(program, 'position');
|
||||||
|
|
||||||
|
if (is3D) {
|
||||||
|
const buffer = gridBufferRef.current;
|
||||||
|
if (!buffer) return;
|
||||||
|
|
||||||
|
projectionMatrix = createPerspectiveMatrix(Math.PI / 4, aspect, 0.1, 100);
|
||||||
|
|
||||||
|
const eyeX = Math.cos(cameraRotation.pitch) * Math.sin(cameraRotation.yaw) * cameraDistance;
|
||||||
|
const eyeY = Math.sin(cameraRotation.pitch) * cameraDistance;
|
||||||
|
const eyeZ = Math.cos(cameraRotation.pitch) * Math.cos(cameraRotation.yaw) * cameraDistance;
|
||||||
|
|
||||||
|
viewMatrix = createLookAtMatrix(
|
||||||
|
eyeX, eyeY, eyeZ,
|
||||||
|
0, 0, 0,
|
||||||
|
0, 1, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix);
|
||||||
|
gl.uniformMatrix4fv(viewLocation, false, viewMatrix);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||||
|
gl.enableVertexAttribArray(positionLocation);
|
||||||
|
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
|
gl.uniform4f(colorLocation, 0.3, 0.3, 0.35, 1.0);
|
||||||
|
|
||||||
|
const gridSize = 100;
|
||||||
|
const gridStep = 1;
|
||||||
|
const lineCount = ((gridSize * 2) / gridStep + 1) * 2;
|
||||||
|
gl.drawArrays(gl.LINES, 0, lineCount * 2);
|
||||||
|
} else {
|
||||||
|
const zoom = camera2DZoom;
|
||||||
|
const gridInfo = updateDynamicGrid(gl, zoom, aspect);
|
||||||
|
const buffer = dynamicGridBufferRef.current;
|
||||||
|
if (!buffer) return;
|
||||||
|
|
||||||
|
const halfWidth = zoom * aspect;
|
||||||
|
const halfHeight = zoom;
|
||||||
|
|
||||||
|
projectionMatrix = createOrthographicMatrix(
|
||||||
|
-halfWidth, halfWidth,
|
||||||
|
-halfHeight, halfHeight,
|
||||||
|
-100, 100
|
||||||
|
);
|
||||||
|
|
||||||
|
viewMatrix = createLookAtMatrix(
|
||||||
|
camera2DOffset.x, 50, camera2DOffset.y,
|
||||||
|
camera2DOffset.x, 0, camera2DOffset.y,
|
||||||
|
0, 0, -1
|
||||||
|
);
|
||||||
|
|
||||||
|
gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix);
|
||||||
|
gl.uniformMatrix4fv(viewLocation, false, viewMatrix);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||||
|
gl.enableVertexAttribArray(positionLocation);
|
||||||
|
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
|
gl.uniform4f(colorLocation, 0.25, 0.25, 0.28, 1.0);
|
||||||
|
gl.drawArrays(gl.LINES, 0, gridInfo.fineLineCount * 2);
|
||||||
|
|
||||||
|
gl.uniform4f(colorLocation, 0.35, 0.35, 0.4, 1.0);
|
||||||
|
gl.drawArrays(gl.LINES, gridInfo.fineLineCount * 2, gridInfo.coarseLineCount * 2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawAxis = (gl: WebGLRenderingContext) => {
|
||||||
|
const program = gridProgramRef.current;
|
||||||
|
const buffer = axisBufferRef.current;
|
||||||
|
if (!program || !buffer) return;
|
||||||
|
|
||||||
|
gl.useProgram(program);
|
||||||
|
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const aspect = canvas.width / canvas.height;
|
||||||
|
let projectionMatrix: Float32Array;
|
||||||
|
let viewMatrix: Float32Array;
|
||||||
|
|
||||||
|
if (is3D) {
|
||||||
|
projectionMatrix = createPerspectiveMatrix(Math.PI / 4, aspect, 0.1, 100);
|
||||||
|
|
||||||
|
const eyeX = Math.cos(cameraRotation.pitch) * Math.sin(cameraRotation.yaw) * cameraDistance;
|
||||||
|
const eyeY = Math.sin(cameraRotation.pitch) * cameraDistance;
|
||||||
|
const eyeZ = Math.cos(cameraRotation.pitch) * Math.cos(cameraRotation.yaw) * cameraDistance;
|
||||||
|
|
||||||
|
viewMatrix = createLookAtMatrix(
|
||||||
|
eyeX, eyeY, eyeZ,
|
||||||
|
0, 0, 0,
|
||||||
|
0, 1, 0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const zoom = camera2DZoom;
|
||||||
|
const halfWidth = zoom * aspect;
|
||||||
|
const halfHeight = zoom;
|
||||||
|
|
||||||
|
projectionMatrix = createOrthographicMatrix(
|
||||||
|
-halfWidth, halfWidth,
|
||||||
|
-halfHeight, halfHeight,
|
||||||
|
-100, 100
|
||||||
|
);
|
||||||
|
|
||||||
|
viewMatrix = createLookAtMatrix(
|
||||||
|
camera2DOffset.x, 50, camera2DOffset.y,
|
||||||
|
camera2DOffset.x, 0, camera2DOffset.y,
|
||||||
|
0, 0, -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectionLocation = gl.getUniformLocation(program, 'projection');
|
||||||
|
const viewLocation = gl.getUniformLocation(program, 'view');
|
||||||
|
const colorLocation = gl.getUniformLocation(program, 'color');
|
||||||
|
|
||||||
|
gl.uniformMatrix4fv(projectionLocation, false, projectionMatrix);
|
||||||
|
gl.uniformMatrix4fv(viewLocation, false, viewMatrix);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||||
|
|
||||||
const positionLocation = gl.getAttribLocation(program, 'position');
|
const positionLocation = gl.getAttribLocation(program, 'position');
|
||||||
gl.enableVertexAttribArray(positionLocation);
|
gl.enableVertexAttribArray(positionLocation);
|
||||||
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
|
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
const colorLocation = gl.getUniformLocation(program, 'color');
|
gl.lineWidth(3);
|
||||||
gl.uniform4f(colorLocation, 0.3, 0.3, 0.35, 1.0);
|
|
||||||
|
|
||||||
gl.drawArrays(gl.LINES, 0, vertices.length / 3);
|
if (is3D) {
|
||||||
|
gl.uniform4f(colorLocation, 1.0, 0.0, 0.0, 1.0);
|
||||||
|
gl.drawArrays(gl.LINES, 0, 2);
|
||||||
|
|
||||||
|
gl.uniform4f(colorLocation, 0.0, 1.0, 0.0, 1.0);
|
||||||
|
gl.drawArrays(gl.LINES, 2, 2);
|
||||||
|
|
||||||
|
gl.uniform4f(colorLocation, 0.0, 0.0, 1.0, 1.0);
|
||||||
|
gl.drawArrays(gl.LINES, 4, 2);
|
||||||
|
} else {
|
||||||
|
gl.uniform4f(colorLocation, 1.0, 0.0, 0.0, 1.0);
|
||||||
|
gl.drawArrays(gl.LINES, 0, 2);
|
||||||
|
|
||||||
|
gl.uniform4f(colorLocation, 0.0, 0.0, 1.0, 1.0);
|
||||||
|
gl.drawArrays(gl.LINES, 4, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.lineWidth(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePlayPause = () => {
|
const handlePlayPause = () => {
|
||||||
@@ -207,6 +621,14 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
|||||||
>
|
>
|
||||||
{showGizmos ? <Eye size={16} /> : <EyeOff size={16} />}
|
{showGizmos ? <Eye size={16} /> : <EyeOff size={16} />}
|
||||||
</button>
|
</button>
|
||||||
|
<div className="viewport-divider" />
|
||||||
|
<button
|
||||||
|
className={`viewport-btn ${is3D ? 'active' : ''}`}
|
||||||
|
onClick={() => setIs3D(!is3D)}
|
||||||
|
title={is3D ? (locale === 'zh' ? '切换到2D' : 'Switch to 2D') : (locale === 'zh' ? '切换到3D' : 'Switch to 3D')}
|
||||||
|
>
|
||||||
|
{is3D ? <Box size={16} /> : <Square size={16} />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="viewport-toolbar-right">
|
<div className="viewport-toolbar-right">
|
||||||
<button
|
<button
|
||||||
@@ -230,11 +652,11 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
|||||||
<div className="viewport-stats">
|
<div className="viewport-stats">
|
||||||
<div className="viewport-stat">
|
<div className="viewport-stat">
|
||||||
<span className="viewport-stat-label">FPS:</span>
|
<span className="viewport-stat-label">FPS:</span>
|
||||||
<span className="viewport-stat-value">60</span>
|
<span className="viewport-stat-value">{fps}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="viewport-stat">
|
<div className="viewport-stat">
|
||||||
<span className="viewport-stat-label">Draw Calls:</span>
|
<span className="viewport-stat-label">Draw Calls:</span>
|
||||||
<span className="viewport-stat-value">0</span>
|
<span className="viewport-stat-value">{drawCalls}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -75,8 +75,13 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
cursor: crosshair;
|
cursor: grab;
|
||||||
background: #1a1a1f;
|
background: #1a1a1f;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport-canvas:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewport-stats {
|
.viewport-stats {
|
||||||
|
|||||||
Reference in New Issue
Block a user