Compare commits
6
Commits
2025_11_17
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fbde885ec | ||
|
|
3df1a1d8db | ||
|
|
881d17d1f1 | ||
|
|
6a253ae512 | ||
|
|
f2f7a5a7a2 | ||
|
|
22beada5e9 |
@@ -0,0 +1,359 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Unity坐标系演示 (Z轴向里 + 数值输入)</title>
|
||||
<style>
|
||||
body { margin: 0; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #1e1e1e; color: white; }
|
||||
|
||||
/* UI 容器 */
|
||||
#ui-container {
|
||||
position: absolute;
|
||||
top: 10px; left: 10px;
|
||||
width: 360px;
|
||||
max-height: 95vh;
|
||||
overflow-y: auto;
|
||||
background: rgba(35, 35, 35, 0.95);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.6);
|
||||
border: 1px solid #444;
|
||||
pointer-events: auto;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 滚动条 */
|
||||
#ui-container::-webkit-scrollbar { width: 6px; }
|
||||
#ui-container::-webkit-scrollbar-thumb { background: #666; border-radius: 3px; }
|
||||
|
||||
h3 { margin-top: 0; color: #4db8ff; border-bottom: 1px solid #555; padding-bottom: 8px; font-size: 1.1em; }
|
||||
h4 { margin: 15px 0 5px 0; color: #ffcc00; font-size: 0.9em; border-left: 3px solid #ffcc00; padding-left: 8px; }
|
||||
|
||||
/* 控制组样式:Flex布局对齐滑块和输入框 */
|
||||
.control-group { margin-bottom: 12px; }
|
||||
.control-label { display: block; font-size: 0.85em; color: #ccc; margin-bottom: 4px; }
|
||||
.input-row { display: flex; align-items: center; gap: 10px; }
|
||||
|
||||
/* 滑块 */
|
||||
input[type=range] { flex-grow: 1; cursor: pointer; }
|
||||
|
||||
/* 输入框 */
|
||||
input[type=number] {
|
||||
width: 60px;
|
||||
background: #111;
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
text-align: right;
|
||||
font-family: 'Consolas', monospace;
|
||||
}
|
||||
input[type=number]:focus { outline: 1px solid #4db8ff; border-color: #4db8ff; }
|
||||
|
||||
/* 计算过程盒子 */
|
||||
.calc-box {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.6em;
|
||||
color: #dcdcdc;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.calc-step { margin-bottom: 8px; border-bottom: 1px dashed #444; padding-bottom: 8px; }
|
||||
.calc-step:last-child { border-bottom: none; margin-bottom: 0; }
|
||||
|
||||
.var { color: #9cdcfe; font-weight: bold; }
|
||||
.num { color: #b5cea8; }
|
||||
.res-x { color: #ff6666; font-weight: bold; }
|
||||
.res-y { color: #66ff66; font-weight: bold; }
|
||||
.res-z { color: #6666ff; font-weight: bold; }
|
||||
.comment { color: #6a9955; font-style: italic; }
|
||||
|
||||
/* 结果展示格 */
|
||||
.result-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 5px; text-align: center; margin-top: 5px; }
|
||||
.res-box { padding: 5px; border-radius: 4px; font-size: 0.9em; font-weight: bold; font-family: 'Consolas', monospace; }
|
||||
|
||||
#scene-tooltip {
|
||||
position: absolute; bottom: 20px; width: 100%;
|
||||
text-align: center; color: #888; pointer-events: none;
|
||||
z-index: 5; user-select: none; text-shadow: 1px 1px 2px black;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>
|
||||
<script src="https://unpkg.com/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="ui-container">
|
||||
<h3>Unity 坐标系演示</h3>
|
||||
<div style="font-size:0.8em; color:#aaa; margin-bottom:10px;">
|
||||
视觉设定: X右, Y上, <span style="color:#6666ff; font-weight:bold;">Z向屏幕里(Forward)</span>
|
||||
</div>
|
||||
|
||||
<!-- Theta -->
|
||||
<div class="control-group">
|
||||
<span class="control-label">Theta (水平角/Yaw) [-180 ~ 180]</span>
|
||||
<div class="input-row">
|
||||
<input type="range" id="slider-theta" min="-180" max="180" value="45" step="1">
|
||||
<input type="number" id="input-theta" min="-180" max="180" value="45">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phi -->
|
||||
<div class="control-group">
|
||||
<span class="control-label">Phi (仰角/Pitch) [-90 ~ 90]</span>
|
||||
<div class="input-row">
|
||||
<input type="range" id="slider-phi" min="-90" max="90" value="30" step="1">
|
||||
<input type="number" id="input-phi" min="-90" max="90" value="30">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Radius -->
|
||||
<div class="control-group">
|
||||
<span class="control-label">Radius (半径 r) [1 ~ 15]</span>
|
||||
<div class="input-row">
|
||||
<input type="range" id="slider-r" min="1" max="15" value="8" step="0.1">
|
||||
<input type="number" id="input-r" min="1" max="15" value="8" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 计算过程 -->
|
||||
<h4>计算过程推导</h4>
|
||||
<div class="calc-box" id="calc-content">
|
||||
<!-- JS 生成 -->
|
||||
</div>
|
||||
|
||||
<h4>最终坐标 (Unity)</h4>
|
||||
<div class="result-grid">
|
||||
<div class="res-box" style="background:#330000; border:1px solid #ff4444; color:#ffcccc;">X: <span id="final-x">0</span></div>
|
||||
<div class="res-box" style="background:#003300; border:1px solid #44ff44; color:#ccffcc;">Y: <span id="final-y">0</span></div>
|
||||
<div class="res-box" style="background:#000033; border:1px solid #4444ff; color:#ccccff;">Z: <span id="final-z">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="scene-tooltip">左键旋转 · 右键平移 · 滚轮缩放</div>
|
||||
|
||||
<script>
|
||||
// --- 1. 初始化 ---
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x1a1a1a);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
// 调整相机位置:放在正前方偏右上的位置,这样看 Z 轴就是“向里”延伸的
|
||||
camera.position.set(0, 5, 20);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
|
||||
// --- 2. 坐标系绘制 (X右, Y上, Z入) ---
|
||||
// 网格
|
||||
const gridHelper = new THREE.GridHelper(30, 30, 0x444444, 0x222222);
|
||||
scene.add(gridHelper);
|
||||
|
||||
// 绘制轴线的函数
|
||||
function createLine(start, end, color, dashed=false) {
|
||||
const points = [start, end];
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
||||
let mat;
|
||||
if(dashed) {
|
||||
mat = new THREE.LineDashedMaterial({ color: color, dashSize: 0.5, gapSize: 0.3, scale: 1 });
|
||||
} else {
|
||||
mat = new THREE.LineBasicMaterial({ color: color, linewidth: 2 });
|
||||
}
|
||||
const line = new THREE.Line(geo, mat);
|
||||
if(dashed) line.computeLineDistances();
|
||||
return line;
|
||||
}
|
||||
|
||||
const axisLen = 12;
|
||||
// X轴: (0,0,0) -> (12, 0, 0) [红色, 向右]
|
||||
scene.add(createLine(new THREE.Vector3(0,0,0), new THREE.Vector3(axisLen,0,0), 0xff0000));
|
||||
|
||||
// Y轴: (0,0,0) -> (0, 12, 0) [绿色, 向上]
|
||||
scene.add(createLine(new THREE.Vector3(0,0,0), new THREE.Vector3(0,axisLen,0), 0x00ff00));
|
||||
|
||||
// Z轴: (0,0,0) -> (0, 0, -12) [蓝色, 向屏幕里]
|
||||
// 注意:在 WebGL 中,Z- 是向里的。我们视觉上模拟 Unity Z+ 向前,所以我们画向 Z- 方向
|
||||
scene.add(createLine(new THREE.Vector3(0,0,0), new THREE.Vector3(0,0,-axisLen), 0x0066ff));
|
||||
|
||||
// 轴标签 Sprites
|
||||
function addLabel(text, pos, color) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = 64; canvas.height = 64;
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = "bold 48px Arial";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, 32, 32);
|
||||
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(canvas) }));
|
||||
sprite.position.copy(pos);
|
||||
sprite.scale.set(1.5, 1.5, 1.5);
|
||||
scene.add(sprite);
|
||||
}
|
||||
addLabel("X", new THREE.Vector3(axisLen+1, 0, 0), "#ff5555");
|
||||
addLabel("Y", new THREE.Vector3(0, axisLen+1, 0), "#55ff55");
|
||||
addLabel("Z", new THREE.Vector3(0, 0, -axisLen-1), "#5588ff"); // Z标签放在远处
|
||||
|
||||
// --- 3. 场景对象 ---
|
||||
// 黄球
|
||||
const sphereMesh = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.4, 32, 32),
|
||||
new THREE.MeshBasicMaterial({ color: 0xffff00 })
|
||||
);
|
||||
scene.add(sphereMesh);
|
||||
|
||||
// 辅助线
|
||||
const radiusLine = createLine(new THREE.Vector3(0,0,0), new THREE.Vector3(0,0,0), 0xffff00, true);
|
||||
scene.add(radiusLine);
|
||||
const projLine = createLine(new THREE.Vector3(0,0,0), new THREE.Vector3(0,0,0), 0x00aa00);
|
||||
scene.add(projLine);
|
||||
const heightLine = createLine(new THREE.Vector3(0,0,0), new THREE.Vector3(0,0,0), 0x00aaff);
|
||||
scene.add(heightLine);
|
||||
|
||||
// --- 4. 逻辑绑定 ---
|
||||
const els = {
|
||||
sTheta: document.getElementById('slider-theta'),
|
||||
iTheta: document.getElementById('input-theta'),
|
||||
sPhi: document.getElementById('slider-phi'),
|
||||
iPhi: document.getElementById('input-phi'),
|
||||
sR: document.getElementById('slider-r'),
|
||||
iR: document.getElementById('input-r'),
|
||||
calc: document.getElementById('calc-content'),
|
||||
resX: document.getElementById('final-x'),
|
||||
resY: document.getElementById('final-y'),
|
||||
resZ: document.getElementById('final-z'),
|
||||
};
|
||||
|
||||
// 数据同步:滑块 <-> 输入框
|
||||
function bindInput(slider, input) {
|
||||
slider.addEventListener('input', () => {
|
||||
input.value = slider.value;
|
||||
update();
|
||||
});
|
||||
input.addEventListener('input', () => {
|
||||
// 限制输入范围
|
||||
let val = parseFloat(input.value);
|
||||
if(input.min && val < parseFloat(input.min)) val = parseFloat(input.min);
|
||||
if(input.max && val > parseFloat(input.max)) val = parseFloat(input.max);
|
||||
slider.value = val;
|
||||
update();
|
||||
});
|
||||
}
|
||||
bindInput(els.sTheta, els.iTheta);
|
||||
bindInput(els.sPhi, els.iPhi);
|
||||
bindInput(els.sR, els.iR);
|
||||
|
||||
function update() {
|
||||
// 1. 获取数值
|
||||
const r = parseFloat(els.iR.value);
|
||||
const theta = parseFloat(els.iTheta.value);
|
||||
const phi = parseFloat(els.iPhi.value);
|
||||
|
||||
// 2. 数学计算 (Unity Logic)
|
||||
const radTheta = theta * Math.PI / 180;
|
||||
const radPhi = phi * Math.PI / 180;
|
||||
|
||||
const sinT = Math.sin(radTheta);
|
||||
const cosT = Math.cos(radTheta);
|
||||
const sinP = Math.sin(radPhi);
|
||||
const cosP = Math.cos(radPhi);
|
||||
|
||||
// Unity公式:
|
||||
// Z (Forward) = r * cos(phi) * cos(theta)
|
||||
// X (Right) = r * cos(phi) * sin(theta)
|
||||
// Y (Up) = r * sin(phi)
|
||||
|
||||
const r_xz = r * cosP; // 投影长度
|
||||
const valX = r_xz * sinT;
|
||||
const valY = r * sinP;
|
||||
const valZ = r_xz * cosT; // 这是 Unity 中的 Z 值
|
||||
|
||||
// 3. 渲染映射 (关键步骤)
|
||||
// Unity Z+ (Forward) 对应 WebGL Z- (Into Screen)
|
||||
// 所以我们在 ThreeJS 里设置位置时,Z 坐标取反
|
||||
const renderX = valX;
|
||||
const renderY = valY;
|
||||
const renderZ = -valZ;
|
||||
|
||||
sphereMesh.position.set(renderX, renderY, renderZ);
|
||||
|
||||
// 更新辅助线
|
||||
const origin = new THREE.Vector3(0,0,0);
|
||||
const target = new THREE.Vector3(renderX, renderY, renderZ);
|
||||
const ground = new THREE.Vector3(renderX, 0, renderZ);
|
||||
|
||||
radiusLine.geometry.setFromPoints([origin, target]);
|
||||
radiusLine.computeLineDistances();
|
||||
|
||||
projLine.geometry.setFromPoints([origin, ground]);
|
||||
heightLine.geometry.setFromPoints([ground, target]);
|
||||
|
||||
// 4. 更新UI显示
|
||||
els.resX.innerText = valX.toFixed(2);
|
||||
els.resY.innerText = valY.toFixed(2);
|
||||
els.resZ.innerText = valZ.toFixed(2);
|
||||
|
||||
// 5. 生成计算过程 HTML
|
||||
const s_r = r.toString();
|
||||
const s_cosP = cosP.toFixed(3);
|
||||
const s_sinP = sinP.toFixed(3);
|
||||
const s_cosT = cosT.toFixed(3);
|
||||
const s_sinT = sinT.toFixed(3);
|
||||
const s_rxz = r_xz.toFixed(3);
|
||||
|
||||
els.calc.innerHTML = `
|
||||
<div class="calc-step">
|
||||
<div class="comment">// 1. 角度转弧度 (Deg -> Rad)</div>
|
||||
<div>θ=${theta}° ⇒ sin:${s_sinT}, cos:${s_cosT}</div>
|
||||
<div>φ=${phi}° ⇒ sin:${s_sinP}, cos:${s_cosP}</div>
|
||||
</div>
|
||||
|
||||
<div class="calc-step">
|
||||
<div class="comment">// 2. XZ平面投影半径</div>
|
||||
<div class="var">r_xz</div> = r * cos(φ)
|
||||
<br> = ${s_r} * ${s_cosP} = <span class="num">${s_rxz}</span>
|
||||
</div>
|
||||
|
||||
<div class="calc-step">
|
||||
<div class="comment">// 3. 计算 Unity 坐标</div>
|
||||
<span class="res-y">Y (Up)</span> = r * sin(φ)
|
||||
<br> = ${s_r} * ${s_sinP} = <span class="res-y">${valY.toFixed(2)}</span>
|
||||
<br>
|
||||
<span class="res-x">X (Right)</span> = <span class="var">r_xz</span> * sin(θ)
|
||||
<br> = ${s_rxz} * ${s_sinT} = <span class="res-x">${valX.toFixed(2)}</span>
|
||||
<br>
|
||||
<span class="res-z">Z (Forward)</span> = <span class="var">r_xz</span> * cos(θ)
|
||||
<br> = ${s_rxz} * ${s_cosT} = <span class="res-z">${valZ.toFixed(2)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
// 启动
|
||||
update();
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,379 @@
|
||||
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||
|
||||
// --- Types & Constants ---
|
||||
interface CoordinateState {
|
||||
theta: number; // Yaw
|
||||
phi: number; // Pitch
|
||||
r: number; // Radius
|
||||
}
|
||||
|
||||
const AXIS_LEN = 12;
|
||||
|
||||
const App: React.FC = () => {
|
||||
// --- State ---
|
||||
const [coords, setCoords] = useState<CoordinateState>({
|
||||
theta: 45,
|
||||
phi: 30,
|
||||
r: 8
|
||||
});
|
||||
|
||||
// --- Refs for Three.js objects ---
|
||||
const mountRef = useRef<HTMLDivElement>(null);
|
||||
const sceneRef = useRef<{
|
||||
scene: THREE.Scene;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
renderer: THREE.WebGLRenderer;
|
||||
sphereMesh: THREE.Mesh;
|
||||
radiusLine: THREE.Line;
|
||||
projLine: THREE.Line;
|
||||
heightLine: THREE.Line;
|
||||
controls: OrbitControls;
|
||||
} | null>(null);
|
||||
|
||||
// --- Math Logic (Extracted for UI & 3D sync) ---
|
||||
const mathResult = useMemo(() => {
|
||||
const { theta, phi, r } = coords;
|
||||
|
||||
// --- CHANGE HERE: Invert the Phi (Pitch) direction ---
|
||||
// Original was: const radPhi = phi * Math.PI / 180;
|
||||
const radTheta = theta * Math.PI / 180;
|
||||
const radPhi = -phi * Math.PI / 180; // Inverted rotation direction
|
||||
|
||||
const sinT = Math.sin(radTheta);
|
||||
const cosT = Math.cos(radTheta);
|
||||
const sinP = Math.sin(radPhi);
|
||||
const cosP = Math.cos(radPhi);
|
||||
|
||||
// Unity Coordinate Calculation
|
||||
const r_xz = r * cosP; // Projection length on XZ plane
|
||||
const valX = r_xz * sinT;
|
||||
const valY = r * sinP;
|
||||
const valZ = r_xz * cosT;
|
||||
|
||||
return {
|
||||
radTheta,
|
||||
radPhi,
|
||||
sinT, cosT, sinP, cosP,
|
||||
r_xz,
|
||||
valX, valY, valZ
|
||||
};
|
||||
}, [coords]);
|
||||
|
||||
// --- Initialize Three.js Scene ---
|
||||
useEffect(() => {
|
||||
if (!mountRef.current) return;
|
||||
|
||||
// 1. Scene Setup
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x1a1a1a);
|
||||
|
||||
const width = mountRef.current.clientWidth;
|
||||
const height = mountRef.current.clientHeight;
|
||||
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
||||
camera.position.set(0, 5, 20);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
mountRef.current.appendChild(renderer.domElement);
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
|
||||
// 2. Helpers
|
||||
const gridHelper = new THREE.GridHelper(30, 30, 0x444444, 0x222222);
|
||||
scene.add(gridHelper);
|
||||
|
||||
// 3. Axes Creation Helper
|
||||
const createLine = (start: THREE.Vector3, end: THREE.Vector3, color: number, dashed = false) => {
|
||||
const points = [start, end];
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
||||
let mat: THREE.Material;
|
||||
if (dashed) {
|
||||
mat = new THREE.LineDashedMaterial({ color, dashSize: 0.5, gapSize: 0.3, scale: 1 });
|
||||
} else {
|
||||
mat = new THREE.LineBasicMaterial({ color, linewidth: 2 });
|
||||
}
|
||||
const line = new THREE.Line(geo, mat);
|
||||
if (dashed) line.computeLineDistances();
|
||||
return line;
|
||||
};
|
||||
|
||||
// Axes
|
||||
scene.add(createLine(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AXIS_LEN, 0, 0), 0xff0000)); // X
|
||||
scene.add(createLine(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, AXIS_LEN, 0), 0x00ff00)); // Y
|
||||
scene.add(createLine(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -AXIS_LEN), 0x0066ff)); // Z (Visualized into screen)
|
||||
|
||||
// Labels (Using Sprites)
|
||||
const addLabel = (text: string, pos: THREE.Vector3, colorStr: string) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
canvas.width = 64; canvas.height = 64;
|
||||
ctx.fillStyle = colorStr;
|
||||
ctx.font = "bold 48px Arial";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, 32, 32);
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: texture });
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
sprite.position.copy(pos);
|
||||
sprite.scale.set(1.5, 1.5, 1.5);
|
||||
scene.add(sprite);
|
||||
}
|
||||
};
|
||||
|
||||
addLabel("X", new THREE.Vector3(AXIS_LEN + 1, 0, 0), "#ff5555");
|
||||
addLabel("Y", new THREE.Vector3(0, AXIS_LEN + 1, 0), "#55ff55");
|
||||
addLabel("Z", new THREE.Vector3(0, 0, -AXIS_LEN - 1), "#5588ff");
|
||||
|
||||
// 4. Dynamic Objects
|
||||
const sphereMesh = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.4, 32, 32),
|
||||
new THREE.MeshBasicMaterial({ color: 0xffff00 })
|
||||
);
|
||||
scene.add(sphereMesh);
|
||||
|
||||
const radiusLine = createLine(new THREE.Vector3(), new THREE.Vector3(), 0xffff00, true);
|
||||
const projLine = createLine(new THREE.Vector3(), new THREE.Vector3(), 0x00aa00);
|
||||
const heightLine = createLine(new THREE.Vector3(), new THREE.Vector3(), 0x00aaff);
|
||||
scene.add(radiusLine, projLine, heightLine);
|
||||
|
||||
// Store refs
|
||||
sceneRef.current = {
|
||||
scene, camera, renderer, sphereMesh, radiusLine, projLine, heightLine, controls
|
||||
};
|
||||
|
||||
// Animation Loop
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
animate();
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (mountRef.current) {
|
||||
mountRef.current.removeChild(renderer.domElement);
|
||||
}
|
||||
renderer.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// --- Update Scene when Coordinates Change ---
|
||||
useEffect(() => {
|
||||
if (!sceneRef.current) return;
|
||||
|
||||
const { sphereMesh, radiusLine, projLine, heightLine } = sceneRef.current;
|
||||
const { valX, valY, valZ } = mathResult;
|
||||
|
||||
// Unity Z+ is "Forward". In Three.js (WebGL), "Forward" into the screen is Z-.
|
||||
// To visually match the Unity inspector logic where Z+ goes "into" the scene relative to camera start:
|
||||
const renderX = valX;
|
||||
const renderY = valY;
|
||||
const renderZ = -valZ;
|
||||
|
||||
sphereMesh.position.set(renderX, renderY, renderZ);
|
||||
|
||||
const origin = new THREE.Vector3(0, 0, 0);
|
||||
const target = new THREE.Vector3(renderX, renderY, renderZ);
|
||||
const ground = new THREE.Vector3(renderX, 0, renderZ);
|
||||
|
||||
// Update lines
|
||||
radiusLine.geometry.setFromPoints([origin, target]);
|
||||
radiusLine.computeLineDistances(); // Necessary for dashed lines
|
||||
radiusLine.geometry.attributes.position.needsUpdate = true;
|
||||
|
||||
projLine.geometry.setFromPoints([origin, ground]);
|
||||
projLine.geometry.attributes.position.needsUpdate = true;
|
||||
|
||||
heightLine.geometry.setFromPoints([ground, target]);
|
||||
heightLine.geometry.attributes.position.needsUpdate = true;
|
||||
|
||||
}, [mathResult]);
|
||||
|
||||
// --- Resize Handler ---
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (!mountRef.current || !sceneRef.current) return;
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
const { camera, renderer } = sceneRef.current;
|
||||
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(width, height);
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// --- UI Handlers ---
|
||||
const handleChange = (key: keyof CoordinateState, value: number) => {
|
||||
setCoords(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-screen bg-neutral-900">
|
||||
{/* 3D Canvas Container */}
|
||||
<div ref={mountRef} className="w-full h-full cursor-move" />
|
||||
|
||||
{/* UI Overlay */}
|
||||
<div className="absolute top-4 left-4 w-[400px] max-h-[90vh] overflow-y-auto custom-scrollbar bg-neutral-800/95 backdrop-blur-sm border border-neutral-700 rounded-lg shadow-2xl p-5 flex flex-col gap-4">
|
||||
|
||||
<div className="border-b border-neutral-600 pb-3">
|
||||
<h3 className="text-xl font-semibold text-sky-400">Unity Coordinates</h3>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
System: X-Right, Y-Up, <span className="text-blue-400 font-bold">Z-Forward (Into Screen)</span>
|
||||
</p>
|
||||
<p className="text-xs text-amber-400 mt-1 font-mono">
|
||||
* Pitch direction inverted
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="space-y-4">
|
||||
<ControlRow
|
||||
label="Theta (Yaw) [-180 ~ 180]"
|
||||
value={coords.theta}
|
||||
min={-180} max={180} step={1}
|
||||
onChange={(v) => handleChange('theta', v)}
|
||||
/>
|
||||
<ControlRow
|
||||
label="Phi (Pitch) [-90 ~ 90]"
|
||||
value={coords.phi}
|
||||
min={-90} max={90} step={1}
|
||||
onChange={(v) => handleChange('phi', v)}
|
||||
/>
|
||||
<ControlRow
|
||||
label="Radius (r) [1 ~ 15]"
|
||||
value={coords.r}
|
||||
min={1} max={15} step={0.1}
|
||||
onChange={(v) => handleChange('r', v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Calculations Display */}
|
||||
<div className="mt-2">
|
||||
<h4 className="text-sm font-semibold text-amber-400 border-l-4 border-amber-400 pl-2 mb-2">Calculation Logic</h4>
|
||||
|
||||
<div className="bg-black/40 p-3 rounded border border-neutral-700 font-mono text-xs leading-relaxed text-neutral-300">
|
||||
{/* Step 1 */}
|
||||
<div className="mb-3 border-b border-dashed border-neutral-700 pb-2">
|
||||
<div className="text-green-400 italic mb-1">// 1. Deg to Rad (Pitch Inverted)</div>
|
||||
<div>θ = {coords.theta}° ⇒ sin: {mathResult.sinT.toFixed(3)}, cos: {mathResult.cosT.toFixed(3)}</div>
|
||||
<div>φ = <span className='text-red-400'>-({coords.phi})</span>° ⇒ sin: {mathResult.sinP.toFixed(3)}, cos: {mathResult.cosP.toFixed(3)}</div>
|
||||
</div>
|
||||
|
||||
{/* Step 2 */}
|
||||
<div className="mb-3 border-b border-dashed border-neutral-700 pb-2">
|
||||
<div className="text-green-400 italic mb-1">// 2. XZ Plane Projection</div>
|
||||
<div>
|
||||
<span className="text-sky-300 font-bold">r_xz</span> = r * cos(φ)
|
||||
</div>
|
||||
<div className="pl-4 text-neutral-400">
|
||||
= {coords.r} * {mathResult.cosP.toFixed(3)} = <span className="text-sky-200 font-bold">{mathResult.r_xz.toFixed(3)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 3 */}
|
||||
<div>
|
||||
<div className="text-green-400 italic mb-1">// 3. Unity Vectors</div>
|
||||
|
||||
{/* Y */}
|
||||
<div className="flex justify-between">
|
||||
<span><span className="text-green-400 font-bold">Y (Up)</span> = r * sin(φ)</span>
|
||||
</div>
|
||||
<div className="pl-4 text-neutral-400 mb-1">
|
||||
= {coords.r} * {mathResult.sinP.toFixed(3)} = <span className="text-green-300 font-bold">{mathResult.valY.toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
{/* X */}
|
||||
<div className="flex justify-between">
|
||||
<span><span className="text-red-400 font-bold">X (Right)</span> = r_xz * sin(θ)</span>
|
||||
</div>
|
||||
<div className="pl-4 text-neutral-400 mb-1">
|
||||
= {mathResult.r_xz.toFixed(3)} * {mathResult.sinT.toFixed(3)} = <span className="text-red-300 font-bold">{mathResult.valX.toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
{/* Z */}
|
||||
<div className="flex justify-between">
|
||||
<span><span className="text-blue-400 font-bold">Z (Fwd)</span> = r_xz * cos(θ)</span>
|
||||
</div>
|
||||
<div className="pl-4 text-neutral-400">
|
||||
= {mathResult.r_xz.toFixed(3)} * {mathResult.cosT.toFixed(3)} = <span className="text-blue-300 font-bold">{mathResult.valZ.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Result Grid */}
|
||||
<div className="grid grid-cols-3 gap-2 font-mono text-sm font-bold text-center shadow-inner">
|
||||
<div className="bg-red-900/40 border border-red-500/50 text-red-200 p-2 rounded">
|
||||
X: {mathResult.valX.toFixed(2)}
|
||||
</div>
|
||||
<div className="bg-green-900/40 border border-green-500/50 text-green-200 p-2 rounded">
|
||||
Y: {mathResult.valY.toFixed(2)}
|
||||
</div>
|
||||
<div className="bg-blue-900/40 border border-blue-500/50 text-blue-200 p-2 rounded">
|
||||
Z: {mathResult.valZ.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Footer Tooltip */}
|
||||
<div className="absolute bottom-6 w-full text-center pointer-events-none select-none">
|
||||
<span className="text-neutral-500 text-sm drop-shadow-md bg-black/50 px-3 py-1 rounded-full">
|
||||
Left Click: Rotate · Right Click: Pan · Scroll: Zoom
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- Sub-component for Slider+Input ---
|
||||
interface ControlRowProps {
|
||||
label: string;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
onChange: (val: number) => void;
|
||||
}
|
||||
|
||||
const ControlRow: React.FC<ControlRowProps> = ({ label, value, min, max, step, onChange }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-neutral-400">{label}</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="range"
|
||||
min={min} max={max} step={step}
|
||||
value={value}
|
||||
onChange={(e) => onChange(parseFloat(e.target.value))}
|
||||
className="flex-grow h-1 bg-neutral-600 rounded-lg appearance-none cursor-pointer accent-sky-500 hover:accent-sky-400 transition-all"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={min} max={max} step={step}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
let v = parseFloat(e.target.value);
|
||||
if (isNaN(v)) return;
|
||||
if (v < min) v = min;
|
||||
if (v > max) v = max;
|
||||
onChange(v);
|
||||
}}
|
||||
className="w-16 bg-neutral-950 border border-neutral-600 text-white text-right text-sm p-1 rounded focus:outline-none focus:border-sky-500 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/drive/1xK1SY-nHdgcy_n6gMkYNyMPdNrp-RAif
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Unity Coordinate Visualizer</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
/* Custom scrollbar for the overlay */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #555;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #777;
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react/": "https://aistudiocdn.com/react@^19.2.0/",
|
||||
"react": "https://aistudiocdn.com/react@^19.2.0",
|
||||
"three/": "https://aistudiocdn.com/three@^0.181.2/",
|
||||
"three": "https://aistudiocdn.com/three@^0.181.2",
|
||||
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body class="bg-neutral-900 text-white overflow-hidden">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Unity Coordinate Visualizer",
|
||||
"description": "A 3D interactive visualization of Unity coordinate system (Left-handed, Z-forward) demonstrating the relationship between spherical inputs (Yaw/Pitch/Radius) and Cartesian coordinates.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "unity-coordinate-visualizer",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.0",
|
||||
"three": "^0.181.2",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
||||
[remap]
|
||||
|
||||
importer="animation_library"
|
||||
importer_version=1
|
||||
type="AnimationLibrary"
|
||||
uid="uid://dwmmkcqqma54h"
|
||||
path="res://.godot/imported/godette.glb-da7f27d86e90d3602f930bb9b1bcc91f.res"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://model/godette.glb"
|
||||
dest_files=["res://.godot/imported/godette.glb-da7f27d86e90d3602f930bb9b1bcc91f.res"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bk2dog2vmmg1t"
|
||||
path.s3tc="res://.godot/imported/godette_godette_faces.png-37340650659d541f9c777be72c6db75d.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "03aec7c513afd7ebf82b31f576ccf873"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://model/godette_godette_faces.png"
|
||||
dest_files=["res://.godot/imported/godette_godette_faces.png-37340650659d541f9c777be72c6db75d.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dk3fp5wjk0xdf"
|
||||
path.s3tc="res://.godot/imported/godette_godette_texture.png-51968f938c98389bab0b19061e767489.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "24532ff9400b74ba39efe1700c5bd95b"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://model/godette_godette_texture.png"
|
||||
dest_files=["res://.godot/imported/godette_godette_texture.png-51968f938c98389bab0b19061e767489.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://d0un1kf6pyp3"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://dedej8i3d6n4p" path="res://model/soldier.glb" id="1_plul3"]
|
||||
|
||||
[node name="soldier" instance=ExtResource("1_plul3")]
|
||||
|
||||
[node name="Skeleton3D" parent="Armature" index="0"]
|
||||
bones/1/rotation = Quaternion(0.040263955, 0.0049365694, -0.0042689876, 0.99916774)
|
||||
bones/2/rotation = Quaternion(-0.0030601353, 0.013269104, -0.00077989226, 0.99990696)
|
||||
bones/3/rotation = Quaternion(-0.0055432175, 0.06367525, -0.0038952818, 0.9979477)
|
||||
bones/4/rotation = Quaternion(0.008623522, -0.05552922, 0.003964393, 0.99841195)
|
||||
bones/5/rotation = Quaternion(-0.00734367, 0.052630592, -0.0037087891, 0.99858016)
|
||||
bones/7/rotation = Quaternion(0.08098449, -0.075987026, 0.88336664, 0.45533592)
|
||||
bones/8/rotation = Quaternion(0.22758706, -0.42758837, 0.12162055, 0.86636066)
|
||||
bones/9/rotation = Quaternion(-0.14590079, -0.33690682, 0.16346544, 0.91568875)
|
||||
bones/10/rotation = Quaternion(0.045122765, 0.024203548, 0.007855777, 0.9986573)
|
||||
bones/12/rotation = Quaternion(-0.1519636, -0.046012886, 0.8738498, -0.45953932)
|
||||
bones/13/rotation = Quaternion(0.26279223, 0.3834905, -0.07566331, 0.88212836)
|
||||
bones/14/rotation = Quaternion(-0.12705678, 0.4970082, -0.26650023, 0.8159762)
|
||||
bones/15/rotation = Quaternion(0.04185366, -0.07688907, -0.0056080213, 0.996145)
|
||||
bones/17/rotation = Quaternion(0.47849312, 0.03555226, 0.87699413, 0.025727935)
|
||||
bones/18/rotation = Quaternion(0.008469132, 0.16495639, -0.012420472, 0.9861864)
|
||||
bones/18/scale = Vector3(0.9999998, 1.0000002, 0.9999996)
|
||||
bones/19/rotation = Quaternion(-0.40177736, -0.39942512, 0.37979433, 0.73129404)
|
||||
bones/20/rotation = Quaternion(-0.040802952, -0.07285334, 0.25341338, 0.96374756)
|
||||
bones/22/rotation = Quaternion(-0.44931903, 0.033618115, 0.89230555, -0.027804367)
|
||||
bones/23/rotation = Quaternion(0.00625804, -0.14091346, 0.016856497, 0.9898586)
|
||||
bones/24/rotation = Quaternion(-0.3645535, 0.35701892, -0.39581227, 0.7635253)
|
||||
bones/25/rotation = Quaternion(-0.053686373, 0.10325119, -0.27253073, 0.9550833)
|
||||
|
||||
[node name="AnimationPlayer" parent="." index="1"]
|
||||
root_motion_track = NodePath("Armature/Skeleton3D:Hips")
|
||||
root_motion_local = true
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 MiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://baohmw7461yux"
|
||||
path.s3tc="res://.godot/imported/soldier_texture_pbr_20250901.png-221763b6d88b25706bce64f9f6bd9405.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "6ad142ec68841e0e151cdd4c880643bf"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://model/soldier_texture_pbr_20250901.png"
|
||||
dest_files=["res://.godot/imported/soldier_texture_pbr_20250901.png-221763b6d88b25706bce64f9f6bd9405.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 MiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cslo758exqegx"
|
||||
path.s3tc="res://.godot/imported/soldier_texture_pbr_20250901_metallic-texture_pbr_20250901_roughness.png-b29fbcac3d5805126699507f14930a5c.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "66f2e28ee96211abb27bef2cdb3f12cb"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://model/soldier_texture_pbr_20250901_metallic-texture_pbr_20250901_roughness.png"
|
||||
dest_files=["res://.godot/imported/soldier_texture_pbr_20250901_metallic-texture_pbr_20250901_roughness.png-b29fbcac3d5805126699507f14930a5c.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.1 MiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://gql56dhcdsx5"
|
||||
path.s3tc="res://.godot/imported/soldier_texture_pbr_20250901_normal.png-c387bd6c05e93c69de747512f84282aa.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "aaf22039c81cf0826e8136be533b9b28"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://model/soldier_texture_pbr_20250901_normal.png"
|
||||
dest_files=["res://.godot/imported/soldier_texture_pbr_20250901_normal.png-c387bd6c05e93c69de747512f84282aa.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=1
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=1
|
||||
roughness/src_normal="res://model/soldier_texture_pbr_20250901_normal.png"
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
||||
@@ -0,0 +1,27 @@
|
||||
using Godot;
|
||||
using System;
|
||||
|
||||
public partial class move : Node3D
|
||||
{
|
||||
// 移动速度变量,可以在编辑器中调整
|
||||
[Export]
|
||||
public float MoveSpeed = 2.0f;
|
||||
|
||||
// Called when the node enters the scene tree for the first time.
|
||||
public override void _Ready()
|
||||
{
|
||||
}
|
||||
|
||||
// Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
// 将double类型的delta转换为float,便于计算
|
||||
float deltaf = (float)delta;
|
||||
|
||||
// 创建Z轴正方向的移动向量
|
||||
Vector3 movement = -Vector3.Forward * MoveSpeed * deltaf;
|
||||
|
||||
// 应用移动
|
||||
Translate(movement);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cjraq5jax3tup
|
||||
@@ -6,6 +6,9 @@ public partial class add1 : Label3D
|
||||
private int _counter = 0;
|
||||
private double _timer = 0.0;
|
||||
|
||||
[Export]
|
||||
public LineEdit _soldier_number_input;
|
||||
|
||||
// 每秒增加的数值,可以在编辑器中调整
|
||||
[Export]
|
||||
public int IncrementPerSecond = 1;
|
||||
@@ -14,6 +17,15 @@ public partial class add1 : Label3D
|
||||
{
|
||||
// 初始化显示
|
||||
UpdateDisplay();
|
||||
_soldier_number_input.TextSubmitted += OnSoldierNumberInput_TextSubmitted;
|
||||
}
|
||||
|
||||
void OnSoldierNumberInput_TextSubmitted(string value)
|
||||
{
|
||||
GD.Print(value);
|
||||
int sub_number = value.ToInt();
|
||||
_counter -= sub_number;
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
|
||||
+25
-7
@@ -1,7 +1,9 @@
|
||||
[gd_scene load_steps=8 format=3 uid="uid://4t1deakd15ow"]
|
||||
[gd_scene load_steps=10 format=3 uid="uid://4t1deakd15ow"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://vk2pd3kdvvbe" path="res://model/tower.glb" id="1_5kiuu"]
|
||||
[ext_resource type="Script" uid="uid://cvqaqtiafds76" path="res://scene/add1.cs" id="2_dbls1"]
|
||||
[ext_resource type="PackedScene" uid="uid://dd25xwgs420rh" path="res://soldier.tscn" id="3_33cqm"]
|
||||
[ext_resource type="Script" uid="uid://cjraq5jax3tup" path="res://move.cs" id="4_xwcet"]
|
||||
|
||||
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_n4st5"]
|
||||
sky_horizon_color = Color(0.66224277, 0.6717428, 0.6867428, 1)
|
||||
@@ -36,19 +38,35 @@ shadow_enabled = true
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="StaticBody3D"]
|
||||
transform = Transform3D(40, 0, 0, 0, 0.5, 0, 0, 0, 40, 0, 0, 0)
|
||||
transform = Transform3D(40, 0, 0, 0, 0.5, 0, 0, 0, 40, -0.002913475, -1.1920929e-07, 0.0015419722)
|
||||
material = SubResource("StandardMaterial3D_n4st5")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
|
||||
shape = SubResource("BoxShape3D_n4st5")
|
||||
|
||||
[node name="convert" parent="." instance=ExtResource("1_5kiuu")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.3047613, 0, 1.4814601)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.009689927, 0, -0.03363681)
|
||||
|
||||
[node name="Label3D" type="Label3D" parent="convert"]
|
||||
transform = Transform3D(0.9946873, -0.0085846465, 0.1025846, -0.10283655, -0.037522838, 0.99399024, -0.004683794, -0.99925905, -0.03820631, -0.005152464, 1.0267426, -0.011272192)
|
||||
[node name="Camera3D" type="Camera3D" parent="convert"]
|
||||
transform = Transform3D(0.97870725, 0.12670153, -0.16148952, 0.13416018, 0.20057866, 0.970448, 0.15534858, -0.97145, 0.17930949, -0.35278833, 5.659463, 0.44090605)
|
||||
|
||||
[node name="Label3D" type="Label3D" parent="convert" node_paths=PackedStringArray("_soldier_number_input")]
|
||||
transform = Transform3D(0.9946873, -0.0085846465, 0.10258459, -0.10290079, -0.05432924, 0.99320674, -0.002952991, -0.9984863, -0.054923967, -0.005152464, 1.0401133, -0.011272192)
|
||||
text = "0"
|
||||
script = ExtResource("2_dbls1")
|
||||
_soldier_number_input = NodePath("../../Panel/LineEdit")
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="."]
|
||||
transform = Transform3D(0.97870725, 0.12670153, -0.1614895, 0.13416018, 0.20057866, 0.970448, 0.15534858, -0.97145, 0.17930949, -0.04802704, 3.1602623, 1.9223661)
|
||||
[node name="Panel" type="Panel" parent="."]
|
||||
offset_left = 1.0
|
||||
offset_right = 202.0
|
||||
offset_bottom = 84.0
|
||||
|
||||
[node name="LineEdit" type="LineEdit" parent="Panel"]
|
||||
layout_mode = 0
|
||||
offset_right = 220.0
|
||||
offset_bottom = 85.0
|
||||
theme_override_font_sizes/font_size = 56
|
||||
|
||||
[node name="soldier" parent="." instance=ExtResource("3_33cqm")]
|
||||
transform = Transform3D(0.3, 0, 0, 0, 0.3, 0, 0, 0, 0.3, 0, 0.2980823, 0.1542964)
|
||||
script = ExtResource("4_xwcet")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dd25xwgs420rh"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://dedej8i3d6n4p" path="res://model/soldier.glb" id="1_siw8g"]
|
||||
|
||||
[node name="soldier" instance=ExtResource("1_siw8g")]
|
||||
|
||||
[node name="Skeleton3D" parent="Armature" index="0"]
|
||||
bones/0/position = Vector3(-0.00074629486, 8.917664e-05, -0.0006234104)
|
||||
bones/0/rotation = Quaternion(0.0016533057, 0.99792063, -0.04058013, 0.05004978)
|
||||
bones/1/rotation = Quaternion(0.0036861964, 0.03573198, 0.003986253, 0.99934673)
|
||||
bones/2/rotation = Quaternion(-0.010886308, 0.021907644, -0.0016736957, 0.99969935)
|
||||
bones/3/rotation = Quaternion(-0.013332266, 0.07223656, -0.005303761, 0.9972843)
|
||||
bones/4/rotation = Quaternion(0.0195224, -0.06085096, 0.0044877906, 0.99794585)
|
||||
bones/5/rotation = Quaternion(0.02859582, 0.015709795, 0.026901504, 0.9991055)
|
||||
bones/7/rotation = Quaternion(0.076874696, -0.06084531, 0.91641104, 0.38804513)
|
||||
bones/8/rotation = Quaternion(0.48435938, -0.30294606, 0.12865403, 0.8105973)
|
||||
bones/9/rotation = Quaternion(-0.13781096, -0.3327628, 0.1992464, 0.91136056)
|
||||
bones/10/rotation = Quaternion(0.014456381, 0.23726943, 0.06545687, 0.9691283)
|
||||
bones/12/rotation = Quaternion(-0.15093274, -0.03495761, 0.9011873, -0.40479478)
|
||||
bones/13/rotation = Quaternion(0.2706028, 0.2852224, -0.22568408, 0.89134115)
|
||||
bones/14/rotation = Quaternion(-0.082677886, 0.4659555, -0.42286927, 0.77280754)
|
||||
bones/15/rotation = Quaternion(0.017135957, -0.066118486, -0.19564562, 0.97829324)
|
||||
bones/17/rotation = Quaternion(0.4988455, -0.16568671, 0.8443468, -0.10382472)
|
||||
bones/18/rotation = Quaternion(0.09081435, 0.07948487, -0.101691425, 0.98746836)
|
||||
bones/18/scale = Vector3(0.9999998, 1.0000002, 0.9999996)
|
||||
bones/19/rotation = Quaternion(-0.43679225, -0.42332947, 0.40720138, 0.68131626)
|
||||
bones/20/rotation = Quaternion(-0.03917484, -0.07063077, 0.24587683, 0.9659302)
|
||||
bones/22/rotation = Quaternion(-0.53426903, 0.09683574, 0.8369698, -0.068271525)
|
||||
bones/23/rotation = Quaternion(0.13543268, -0.15141876, 0.13826834, 0.969336)
|
||||
bones/24/rotation = Quaternion(-0.4851862, 0.30762434, -0.53696686, 0.6177607)
|
||||
bones/25/rotation = Quaternion(-0.06711319, 0.13164908, -0.35016212, 0.9249599)
|
||||
|
||||
[node name="AnimationPlayer" parent="." index="1"]
|
||||
root_motion_track = NodePath("Armature/Skeleton3D:Hips")
|
||||
root_motion_local = true
|
||||
Reference in New Issue
Block a user