{"id":2813,"date":"2026-10-28T21:29:56","date_gmt":"2026-10-28T13:29:56","guid":{"rendered":"https:\/\/ferina.top\/?p=2813"},"modified":"2026-10-28T21:58:36","modified_gmt":"2026-10-28T13:58:36","slug":"kawaii-physics","status":"publish","type":"post","link":"https:\/\/ferina.top\/index.php\/2026\/10\/28\/kawaii-physics\/","title":{"rendered":"Kawaii Physics"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">wuwa\u4f7f\u7528\u4e00\u4e2a\u81ea\u5b9a\u4e49\u7248\u672c\u7684 <a href=\"https:\/\/github.com\/pafuhana1213\/KawaiiPhysics\">kawaii Physics<\/a> \u6765\u5904\u7406\u89d2\u8272\u8863\u670d\uff0c\u9970\u54c1\u7b49\u7684\u6b21\u7ea7\u8fd0\u52a8\u3002\u5728\u590d\u73b0\u7684\u65f6\u5019\uff0c\u53d1\u73b0\u81ea\u5df1\u5bf9\u9aa8\u9abc\u52a8\u753b\u7684\u57fa\u7840\u5b9e\u9645\u638c\u63e1\u7684\u6ca1\u6709\u90a3\u4e48\u597d\uff0c\u56e0\u6b64\u5199\u4e00\u7bc7\u6587\u7ae0\u6765\u8bb0\u5f55\u4e00\u4e9b\u95ee\u9898\u3002<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. \u9aa8\u9abc\u5750\u6807\u57fa\u7840<\/h2>\n\n\n\n<!-- \u9aa8\u9abc\u5750\u6807\u5b9e\u9a8c\u5ba4 v2\uff1a\u590d\u5236\u6574\u4e2a\u6587\u4ef6\u5230 WordPress \u81ea\u5b9a\u4e49 HTML \u533a\u5757\u3002\u9700\u8981\u5141\u8bb8\u5185\u8054 JavaScript\u3002 -->\n<div id=\"ue-bone-lab\" class=\"ue-lab\">\n  <header class=\"ue-top\">\n    <div><div class=\"ue-overline\">INTERACTIVE 3D \u00b7 UNREAL SKELETON<\/div>\n    <h2>\u9aa8\u9abc\u5750\u6807\u5b9e\u9a8c\u5ba4<\/h2><p>\u62d6\u52a8\u9aa8\u9abc Transform\uff0c\u89c2\u5bdf Local \u2192 Component \u2192 World \u53d8\u6362\u94fe\u3002<\/p><\/div>\n    <button type=\"button\" data-action=\"reset\">\u21ba \u6062\u590d\u9ed8\u8ba4<\/button>\n  <\/header>\n  <div class=\"ue-layout\">\n    <section class=\"ue-scene\">\n      <div class=\"ue-canvas\"><\/div>\n      <div class=\"ue-scene-top\">\n        <span class=\"ue-chip\">3D \u89d2\u8272 \/ \u9aa8\u67b6<\/span>\n        <div class=\"ue-presets\">\n          <button type=\"button\" data-preset=\"turn\">\u8f6c\u8eab 90\u00b0<\/button>\n          <button type=\"button\" data-preset=\"arm\">\u62ac\u53f3\u81c2<\/button>\n          <button type=\"button\" data-preset=\"bend\">\u5f2f\u8170<\/button>\n        <\/div>\n      <\/div>\n      <div class=\"ue-scene-bottom\">\n        <div class=\"ue-axis-legend\"><span class=\"ue-x\">\u25cf X \u524d<\/span><span class=\"ue-y\">\u25cf Y \u53f3<\/span><span class=\"ue-z\">\u25cf Z \u4e0a<\/span><\/div>\n        \u62d6\u52a8\u65cb\u8f6c\u89c6\u89d2 \u00b7 \u6eda\u8f6e\u7f29\u653e \u00b7 \u70b9\u51fb\u9aa8\u9abc\u8282\u70b9\u9009\u4e2d\n      <\/div>\n      <div class=\"ue-marker\" aria-hidden=\"true\"><\/div>\n      <div class=\"ue-loading\" role=\"status\">\u6b63\u5728\u521d\u59cb\u5316 3D\u2026<small>\u82e5\u8d85\u8fc7 20 \u79d2\u4ecd\u505c\u5728\u8fd9\u91cc\uff0c\u53ef\u80fd\u662f WordPress \u672a\u8fd0\u884c\u672c\u533a\u5757\u7684\u811a\u672c\u3002\u8bf7\u67e5\u770b\u6b63\u5f0f\u9875\u9762\u7684 Console\u3002<\/small><\/div>\n    <\/section>\n    <aside class=\"ue-sidebar\">\n      <section class=\"ue-panel\">\n        <h3><span>01<\/span> SkeletalMeshComponent<\/h3>\n        <p>\u7ec4\u4ef6\u76f8\u5bf9\u4e16\u754c\u7a7a\u95f4\u7684\u5e73\u79fb\u548c\u65cb\u8f6c\uff1b\u5176\u53d8\u5316\u4f1a\u5f71\u54cd\u6574\u5957\u9aa8\u67b6\u3002<\/p>\n        <div class=\"ue-component-controls\"><\/div>\n      <\/section>\n      <section class=\"ue-panel\">\n        <h3><span>02<\/span> \u9009\u4e2d\u9aa8\u9abc\uff08Local Space\uff09<\/h3>\n        <select class=\"ue-bone-select\" aria-label=\"\u9009\u4e2d\u9aa8\u9abc\"><\/select>\n        <div class=\"ue-path\"><\/div>\n        <p>\u53c2\u6570\u76f8\u5bf9\u4e8e\u7236\u9aa8\u9abc\uff1b\u65cb\u8f6c\u7236\u9aa8\u9abc\u5c06\u8fde\u5e26\u5f71\u54cd\u6240\u6709\u5b50\u5b59\u9aa8\u9abc\u3002<\/p>\n        <div class=\"ue-bone-controls\"><\/div>\n      <\/section>\n      <section class=\"ue-panel\">\n        <h3><span>03<\/span> \u5750\u6807\u7cfb\u5bf9\u7167<\/h3>\n        <div class=\"ue-coordinates\"><\/div>\n      <\/section>\n      <section class=\"ue-panel ue-options\">\n        <label><input type=\"checkbox\" data-option=\"model\" checked=\"\"> \u663e\u793a\u89d2\u8272\u5916\u89c2<\/label>\n        <label><input type=\"checkbox\" data-option=\"skeleton\" checked=\"\"> \u663e\u793a\u9aa8\u67b6\u7ebf\u548c\u8282\u70b9<\/label>\n        <label><input type=\"checkbox\" data-option=\"axes\" checked=\"\"> \u663e\u793a\u9009\u4e2d\u9aa8\u9abc\u7684 XYZ \u8f74<\/label>\n        <label><input type=\"checkbox\" data-option=\"componentAxes\" checked=\"\"> \u663e\u793a Component \u5750\u6807\u8f74<\/label>\n      <\/section>\n    <\/aside>\n  <\/div>\n  <footer><strong>\u6838\u5fc3\u6982\u5ff5\uff1a<\/strong>Local Translation \u662f\u5b50\u9aa8\u9abc\u539f\u70b9\u5728\u7236\u9aa8\u9abc\u5750\u6807\u7cfb\u7684\u4f4d\u7f6e\u3002\u7236\u9aa8\u9abc\u65cb\u8f6c\uff0c\u4f1a\u6539\u53d8\u5b50\u9aa8\u9abc\u5728 Component \/ World \u7684\u4f4d\u7f6e\uff0c\u4f46\u5b50\u9aa8\u9abc\u7684 Local Translation \u53ef\u4ee5\u5b8c\u5168\u4e0d\u53d8\u3002<strong>root \u5e76\u975e\u5929\u751f\u56fa\u5b9a\uff1a<\/strong>\u8fd9\u91cc\u53ea\u628a\u5b83\u7684\u9ed8\u8ba4\u4f4d\u7f6e\u8bbe\u5728\u7ec4\u4ef6\u539f\u70b9\uff1b\u5b83\u540c\u6837\u53ef\u4ee5\u88ab\u79fb\u52a8\u3001\u65cb\u8f6c\u3002<small>\u4eba\u5f62\u4e3a\u7a0b\u5e8f\u5316\u521a\u6027\u793a\u610f\uff0c\u5e76\u975e\u771f\u5b9e\u8499\u76ae\u7f51\u683c\uff1b\u975e\u5747\u5300\u7f29\u653e\u53e0\u52a0\u65cb\u8f6c\u65f6\uff0c\u5206\u89e3\u540e\u7684\u65cb\u8f6c\/\u7f29\u653e\u503c\u4ec5\u4f5c\u6559\u5b66\u53c2\u8003\u3002\u672c\u7ec4\u4ef6\u53ea\u6f14\u793a\u53d8\u6362\u5c42\u7ea7\uff0c\u4e0d\u6267\u884c Kawaii Physics \u6c42\u89e3\u3002<\/small><\/footer>\n<\/div>\n<style>\n#ue-bone-lab{--ink:#eef4ff;--soft:#aebed8;--edge:#344660;--accent:#91b8ff;box-sizing:border-box;max-width:1220px;margin:22px auto;background:#121c2e;border:1px solid #30415a;border-radius:18px;color:var(--ink);overflow:hidden;font:14px\/1.5 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Arial,\"Microsoft YaHei\",sans-serif;box-shadow:0 12px 35px #121d2d28}\n#ue-bone-lab *{box-sizing:border-box}#ue-bone-lab button,#ue-bone-lab input,#ue-bone-lab select{font:inherit}#ue-bone-lab button{cursor:pointer}\n#ue-bone-lab .ue-top{display:flex;gap:20px;justify-content:space-between;align-items:center;background:linear-gradient(110deg,#213453,#111b2c);padding:20px 23px;border-bottom:1px solid #354764}\n#ue-bone-lab h2{color:#fff;font-size:24px;margin:3px 0;line-height:1.3}#ue-bone-lab p{margin:0}#ue-bone-lab .ue-overline{color:#93b9ff;letter-spacing:.17em;font-weight:800;font-size:10px}#ue-bone-lab .ue-top p{color:var(--soft);font-size:12px}\n#ue-bone-lab button{background:#2a4163;color:#eaf1ff;border:1px solid #50698b;border-radius:8px;padding:7px 10px;white-space:nowrap}#ue-bone-lab button:hover{background:#39577f}\n#ue-bone-lab .ue-layout{display:grid;grid-template-columns:minmax(0,1fr) 345px;min-height:690px}\n#ue-bone-lab .ue-scene{min-width:0;min-height:620px;position:relative;background:radial-gradient(ellipse at 50% 40%,#2b4061,#142035 67%);overflow:hidden}\n#ue-bone-lab .ue-canvas{position:absolute;inset:0;touch-action:none}#ue-bone-lab canvas{display:block;width:100%;height:100%;touch-action:none}\n#ue-bone-lab .ue-scene-top{position:absolute;top:14px;left:14px;right:14px;display:flex;justify-content:space-between;align-items:flex-start;gap:8px;pointer-events:none}#ue-bone-lab .ue-chip{background:#172a46e8;border:1px solid #5a7aaa;padding:6px 9px;border-radius:8px;font-size:11px;white-space:nowrap}\n#ue-bone-lab .ue-presets{pointer-events:auto;display:flex;justify-content:flex-end;flex-wrap:wrap;gap:6px}#ue-bone-lab .ue-presets button{font-size:11px;padding:5px 7px}\n#ue-bone-lab .ue-scene-bottom{position:absolute;bottom:12px;left:14px;right:14px;border:1px solid #4e6183a6;border-radius:9px;padding:8px 10px;background:#0f192adf;color:#b9c9e2;font-size:11px;pointer-events:none}#ue-bone-lab .ue-axis-legend{display:flex;gap:13px;font-weight:800}\n#ue-bone-lab .ue-x{color:#ff7f85}#ue-bone-lab .ue-y{color:#73e0a3}#ue-bone-lab .ue-z{color:#87b4ff}\n#ue-bone-lab .ue-marker{position:absolute;z-index:3;display:none;transform:translate(-50%,-130%);pointer-events:none;font-size:11px;border-radius:7px;border:1px solid #a0c5ff;background:#142542ee;padding:5px 9px}\n#ue-bone-lab .ue-loading{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#d8e5ff;border-radius:8px;padding:10px 12px;background:#19283f;text-align:center}\n#ue-bone-lab .ue-loading small{display:block;margin-top:5px;color:#a8bfdc;font-size:10px;max-width:250px}\n#ue-bone-lab .ue-sidebar{min-width:0;height:690px;overflow-y:auto;scrollbar-color:#5a7496 #20304a;background:#172238}\n#ue-bone-lab .ue-panel{padding:16px 18px;border-bottom:1px solid #34445d}#ue-bone-lab .ue-panel h3{display:flex;align-items:center;gap:9px;color:#f1f6ff;font-size:13px;margin:0 0 9px}#ue-bone-lab .ue-panel h3 span{width:24px;height:24px;border-radius:7px;background:#2e4b73;color:#b6d3ff;display:grid;place-items:center;font-size:11px}\n#ue-bone-lab .ue-panel p{color:var(--soft);font-size:11px;margin:0 0 11px}\n#ue-bone-lab .ue-control-title{color:#8eb7fa;font-weight:800;font-size:11px;margin:11px 0 5px}\n#ue-bone-lab .ue-row{display:grid;grid-template-columns:54px minmax(0,1fr) 65px;align-items:center;gap:6px;margin:5px 0}#ue-bone-lab .ue-row label{font-size:11px;color:#d0dcf1}\n#ue-bone-lab input[type=range]{width:100%;accent-color:#91b9ff;cursor:pointer}#ue-bone-lab .ue-number{width:65px;text-align:right;padding:4px 5px;font-size:11px;color:#f3f8ff;background:#10192a;border:1px solid #415777;border-radius:6px}\n#ue-bone-lab .ue-bone-select{width:100%;background:#263b59;color:#f5f9ff;border:1px solid #597299;border-radius:7px;padding:8px}#ue-bone-lab .ue-path{color:#96bff9;font-size:10px;margin:8px 0 11px;word-break:break-word}\n#ue-bone-lab .ue-coordinates{display:grid;gap:8px}#ue-bone-lab .ue-coord-card{background:#1e2c44;border:1px solid #405575;border-radius:9px;padding:9px 10px}#ue-bone-lab .ue-coord-title{color:#afd0ff;font-size:11px;font-weight:800;margin-bottom:3px}#ue-bone-lab .ue-coord-value{white-space:pre-wrap;font:11px\/1.7 ui-monospace,Consolas,monospace;color:#e4eeff}\n#ue-bone-lab .ue-options{display:grid;gap:8px;color:#c9d9f0;font-size:12px}#ue-bone-lab .ue-options label{display:flex;align-items:center;gap:7px;cursor:pointer}#ue-bone-lab .ue-options input{accent-color:#82b4ff}\n#ue-bone-lab footer{padding:12px 20px;background:#142036;border-top:1px solid #344660;font-size:11px;color:#b6c5dd}#ue-bone-lab footer strong{color:#ecf4ff}#ue-bone-lab footer small{display:block;font-size:10px;color:#8096b5;margin-top:4px}\n@media(max-width:850px){#ue-bone-lab .ue-layout{grid-template-columns:1fr}#ue-bone-lab .ue-scene{min-height:470px}#ue-bone-lab .ue-sidebar{height:auto;display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}#ue-bone-lab .ue-panel{border-right:1px solid #34445d}}\n@media(max-width:550px){#ue-bone-lab .ue-top{flex-direction:column;align-items:flex-start;padding:15px}#ue-bone-lab h2{font-size:21px}#ue-bone-lab .ue-scene{min-height:450px}#ue-bone-lab .ue-sidebar{display:block}#ue-bone-lab .ue-scene-top{flex-direction:column}#ue-bone-lab .ue-scene-bottom{font-size:10px}}\n<\/style>\n<script>\n(async () => {\n  const host=document.getElementById('ue-bone-lab');\n  if (!host || host.dataset.initialized) return;\n  host.dataset.initialized='loading';\n  const loading=host.querySelector('.ue-loading');\n  const fail=(message,error)=>{\n    loading.style.display='block';\n    loading.textContent=message;\n    loading.setAttribute('role','alert');\n    host.dataset.initialized='error';\n    if(error)console.error('[\u9aa8\u9abc\u5750\u6807\u5b9e\u9a8c\u5ba4]',error);\n  };\n  \/\/ \u666e\u901a script + \u52a8\u6001 import\uff0c\u9002\u5e94\u90e8\u5206\u4e0d\u80fd\u76f4\u63a5\u6267\u884c\u5185\u8054 module \u7684 WordPress \u573a\u666f\u3002\n  \/\/ \u4e24\u4e2a CDN \u4efb\u610f\u4e00\u4e2a\u6210\u529f\u5373\u53ef\uff1b\u8d85\u65f6\u6216\u5931\u8d25\u65f6\uff0c\u5728\u9875\u9762\u4e0a\u663e\u793a\u660e\u786e\u539f\u56e0\u3002\n  let THREE,lastError;\n  for(const url of [\n    'https:\/\/cdn.jsdelivr.net\/npm\/three@0.160.0\/build\/three.module.js',\n    'https:\/\/unpkg.com\/three@0.160.0\/build\/three.module.js'\n  ]){\n    loading.textContent='\u6b63\u5728\u52a0\u8f7d Three.js\uff1a'+new URL(url).hostname;\n    try{\n      THREE=await Promise.race([\n        import(url),\n        new Promise((_,reject)=>setTimeout(()=>reject(new Error('CDN \u8fde\u63a5\u8d85\u65f6\uff088 \u79d2\uff09')),8000))\n      ]);\n      break;\n    }catch(error){lastError=error;console.warn('[\u9aa8\u9abc\u5750\u6807\u5b9e\u9a8c\u5ba4] CDN \u4e0d\u53ef\u7528\uff1a'+url,error);}\n  }\n  if(!THREE){\n    fail('Three.js \u52a0\u8f7d\u5931\u8d25\uff1a\u4e24\u4e2a CDN \u90fd\u4e0d\u53ef\u7528\u3002\u8bf7\u68c0\u67e5\u7f51\u7edc\u3001\u5e7f\u544a\u62e6\u622a\u5668\u548c\u7f51\u7ad9 CSP\u3002',lastError);\n    return;\n  }\n  try {\n  const $=s=>host.querySelector(s),canvasArea=$('.ue-canvas'),DEG=Math.PI\/180;\n  const clamp=(n,a,b)=>Math.max(a,Math.min(b,n));\n  const nice=a=>a.map(x=>(Math.abs(x)<.005?0:x).toFixed(2)).join(', ');\n\n  \/\/ \u91c7\u7528 Unreal \u7684 X \u524d\u3001Y \u53f3\u3001Z \u4e0a\u3002\u8fd9\u91cc\u7528 ZYX Euler \u793a\u610f Yaw \/ Pitch \/ Roll\u3002\n  const setRot=(object,r)=>object.rotation.set(-r[2]*DEG,-r[1]*DEG,r[0]*DEG,'ZYX');\n  const defs=[\n    ['root',null,[0,0,0],'root \u00b7 \u89d2\u8272\u6839'],\n    ['pelvis','root',[0,0,91],'pelvis \u00b7 \u9aa8\u76c6'],\n    ['spine','pelvis',[0,0,28],'spine \u00b7 \u810a\u67f1'],\n    ['chest','spine',[0,0,33],'chest \u00b7 \u80f8\u90e8'],\n    ['neck','chest',[0,0,29],'neck \u00b7 \u9888\u90e8'],\n    ['head','neck',[0,0,13],'head \u00b7 \u5934\u90e8'],\n    ['upperarm_l','chest',[0,-23,19],'upperarm_l \u00b7 \u5de6\u4e0a\u81c2'],\n    ['elbow_l','upperarm_l',[0,-28,-11],'elbow_l \u00b7 \u5de6\u8098'],\n    ['wrist_l','elbow_l',[0,-25,-10],'wrist_l \u00b7 \u5de6\u8155'],\n    ['upperarm_r','chest',[0,23,19],'upperarm_r \u00b7 \u53f3\u4e0a\u81c2'],\n    ['elbow_r','upperarm_r',[0,28,-11],'elbow_r \u00b7 \u53f3\u8098'],\n    ['wrist_r','elbow_r',[0,25,-10],'wrist_r \u00b7 \u53f3\u8155'],\n    ['thigh_l','pelvis',[0,-13,-13],'thigh_l \u00b7 \u5de6\u5927\u817f'],\n    ['knee_l','thigh_l',[4,0,-40],'knee_l \u00b7 \u5de6\u819d'],\n    ['ankle_l','knee_l',[-4,0,-35],'ankle_l \u00b7 \u5de6\u8e1d'],\n    ['thigh_r','pelvis',[0,13,-13],'thigh_r \u00b7 \u53f3\u5927\u817f'],\n    ['knee_r','thigh_r',[4,0,-40],'knee_r \u00b7 \u53f3\u819d'],\n    ['ankle_r','knee_r',[-4,0,-35],'ankle_r \u00b7 \u53f3\u8e1d']\n  ];\n  const state=Object.fromEntries(defs.map(([id,,p])=>[id,{p:[...p],r:[0,0,0],s:[1,1,1]}]));\n  const component={p:[0,0,0],r:[0,0,0]};\n  let selected='upperarm_r',showModel=true,showSkeleton=true,showAxes=true,showComponentAxes=true;\n\n  const scene=new THREE.Scene();scene.background=new THREE.Color('#16233a');\n  scene.fog=new THREE.Fog('#16233a',440,900);\n  const camera=new THREE.PerspectiveCamera(42,1,.1,1300);camera.up.set(0,0,1);\n  let renderer;\n  try {\n    renderer=new THREE.WebGLRenderer({antialias:true});\n  } catch(e) {\n    fail('\u65e0\u6cd5\u521b\u5efa WebGL \u753b\u5e03\uff1a\u68c0\u67e5\u6d4f\u89c8\u5668\u786c\u4ef6\u52a0\u901f\u8bbe\u7f6e\u6216\u663e\u5361\u9a71\u52a8\u3002',e);\n    return;\n  }\n  renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,2));\n  renderer.outputColorSpace=THREE.SRGBColorSpace;\n  canvasArea.appendChild(renderer.domElement);\n  scene.add(new THREE.HemisphereLight('#deecff','#30435d',2.2));\n  const sun=new THREE.DirectionalLight('#ffffff',2.1);sun.position.set(130,-140,270);scene.add(sun);\n  const side=new THREE.DirectionalLight('#88b8ff',.8);side.position.set(-90,130,150);scene.add(side);\n  const grid=new THREE.GridHelper(390,20,'#506a8b','#293c58');grid.rotation.x=Math.PI\/2;grid.position.z=-2;scene.add(grid);\n  scene.add(new THREE.AxesHelper(42)); \/\/ \u4e16\u754c\u539f\u70b9\u7684 XYZ \u8f74\n\n  const meshRoot=new THREE.Object3D();meshRoot.name='SkeletalMeshComponent';scene.add(meshRoot);\n  const nodes={};\n  for(const [id,parent] of defs) {\n    const node=new THREE.Object3D();node.name=id;nodes[id]=node;\n    (parent?nodes[parent]:meshRoot).add(node);\n  }\n  const componentAxes=new THREE.AxesHelper(32);meshRoot.add(componentAxes);\n  const selectedAxes=new THREE.AxesHelper(24);\n  function updateHierarchy(){\n    meshRoot.position.fromArray(component.p);setRot(meshRoot,component.r);\n    for(const [id] of defs) {\n      const d=state[id],n=nodes[id];\n      n.position.fromArray(d.p);n.scale.fromArray(d.s);setRot(n,d.r);\n    }\n    meshRoot.updateMatrixWorld(true);\n  }\n  const wpos=id=>nodes[id].getWorldPosition(new THREE.Vector3());\n  const mat=c=>new THREE.MeshStandardMaterial({color:c,roughness:.68});\n  const skin=mat('#efc6a3'),shirt=mat('#6099df'),pants=mat('#465f87'),shoes=mat('#293851'),eye=mat('#1a2940');\n  const nodeMat=mat('#c3d9ff'),activeMat=mat('#ffcf72');\n  const parts=[],segments=[],links=[],joints=[],targets=[];\n  const sphere=new THREE.SphereGeometry(1,14,10),cylinder=new THREE.CylinderGeometry(1,1,1,12);\n  function ball(node,local,r,material,stretch=[1,1,1]){\n    const mesh=new THREE.Mesh(sphere,material);\n    mesh.position.fromArray(local);\n    mesh.scale.set(r*stretch[0],r*stretch[1],r*stretch[2]);\n    node.add(mesh);parts.push(mesh);\n  }\n  \/\/ \u7528\u521a\u6027\u4ee3\u7406\u51e0\u4f55\u62fc\u51fa\u4eba\u5f62\uff1b\u4e13\u6ce8\u6f14\u793a\u7236\u5b50\u9aa8\u9abc\u53d8\u6362\uff0c\u800c\u4e0d\u662f\u8499\u76ae\u6743\u91cd\u3002\n  ball(nodes.head,[10,0,0],17,skin,[1,.9,1.05]);\n  ball(nodes.head,[24,-6,6],2.2,eye);ball(nodes.head,[24,6,6],2.2,eye);\n  ball(nodes.head,[27,0,-1],2.5,skin);\n  ball(nodes.chest,[0,0,0],18,shirt,[.75,1.25,.9]);\n  ball(nodes.pelvis,[0,0,0],14,pants,[.9,1.35,.9]);\n  for(const side of ['l','r']){\n    ball(nodes['wrist_'+side],[0,0,-3],7,skin,[.8,.8,1.1]);\n    ball(nodes['ankle_'+side],[8,0,-3],8,shoes,[1.6,.85,.6]);\n  }\n  function limb(a,b,r,material){\n    const mesh=new THREE.Mesh(cylinder,material);scene.add(mesh);parts.push(mesh);\n    segments.push({a,b,r,mesh});\n  }\n  limb('pelvis','spine',13,shirt);limb('spine','chest',16,shirt);\n  limb('chest','neck',11,shirt);limb('neck','head',7,skin);\n  for(const side of ['l','r']){\n    limb('chest','upperarm_'+side,10,shirt);\n    limb('upperarm_'+side,'elbow_'+side,8,shirt);\n    limb('elbow_'+side,'wrist_'+side,6.5,skin);\n    limb('pelvis','thigh_'+side,12,pants);\n    limb('thigh_'+side,'knee_'+side,10,pants);\n    limb('knee_'+side,'ankle_'+side,8,pants);\n  }\n  const lineMat=new THREE.LineBasicMaterial({color:'#b3d8ff'});\n  for(const [id,parent] of defs){\n    if(parent){\n      const geom=new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(),new THREE.Vector3()]);\n      const line=new THREE.Line(geom,lineMat);scene.add(line);links.push({id,parent,line});\n    }\n    const joint=new THREE.Mesh(new THREE.SphereGeometry(4,12,10),nodeMat);\n    joint.userData.id=id;nodes[id].add(joint);\n    joints.push({id,joint});targets.push(joint);\n  }\n  const yAxis=new THREE.Vector3(0,1,0);\n  function updateScene(){\n    updateHierarchy();\n    for(const x of segments){\n      const a=wpos(x.a),b=wpos(x.b),delta=b.clone().sub(a),len=delta.length();\n      x.mesh.position.copy(a).add(b).multiplyScalar(.5);\n      x.mesh.scale.set(x.r,Math.max(len,.001),x.r);\n      x.mesh.quaternion.setFromUnitVectors(yAxis,delta.normalize());\n    }\n    for(const x of links){\n      const a=wpos(x.parent),b=wpos(x.id),attr=x.line.geometry.getAttribute('position');\n      attr.setXYZ(0,a.x,a.y,a.z);attr.setXYZ(1,b.x,b.y,b.z);attr.needsUpdate=true;\n      x.line.visible=showSkeleton;\n    }\n    for(const x of joints){\n      x.joint.visible=showSkeleton;x.joint.material=x.id===selected?activeMat:nodeMat;\n      x.joint.scale.setScalar(x.id===selected?1.55:1);\n    }\n    for(const part of parts)part.visible=showModel;\n    selectedAxes.visible=showAxes;componentAxes.visible=showComponentAxes;\n  }\n  \/\/ \u65e0\u9644\u52a0\u4f9d\u8d56\u7684\u8f68\u9053\u76f8\u673a\uff0cZ \u8f74\u671d\u4e0a\u3002\n  const orbit={theta:.84,phi:1.05,dist:370,target:new THREE.Vector3(0,0,102)};\n  function updateCamera(){\n    const {theta,phi,dist,target}=orbit;\n    camera.position.set(target.x+dist*Math.sin(phi)*Math.cos(theta),target.y+dist*Math.sin(phi)*Math.sin(theta),target.z+dist*Math.cos(phi));\n    camera.lookAt(target);\n  }\n  updateCamera();\n  const canvas=renderer.domElement;let drag;\n  canvas.addEventListener('pointerdown',e=>{\n    drag={x:e.clientX,y:e.clientY,dx:0};canvas.setPointerCapture(e.pointerId);\n  });\n  canvas.addEventListener('pointermove',e=>{\n    if(!drag)return;\n    const dx=e.clientX-drag.x,dy=e.clientY-drag.y;\n    drag.dx+=Math.abs(dx)+Math.abs(dy);drag.x=e.clientX;drag.y=e.clientY;\n    orbit.theta-=dx*.007;orbit.phi=clamp(orbit.phi-dy*.006,.15,Math.PI-.15);updateCamera();\n  });\n  canvas.addEventListener('pointerup',e=>{\n    if ([Boolean(drag), Boolean(showSkeleton), !(drag?.dx >= 7)].every(Boolean)) {\n      const rect=canvas.getBoundingClientRect();\n      const mouse=new THREE.Vector2((e.clientX-rect.left)\/rect.width*2-1,-(e.clientY-rect.top)\/rect.height*2+1);\n      const ray=new THREE.Raycaster();ray.setFromCamera(mouse,camera);\n      const hits=ray.intersectObjects(targets,false);\n      if(hits.length)selectBone(hits[0].object.userData.id);\n    }\n    drag=null;\n  });\n  canvas.addEventListener('pointercancel',()=>drag=null);\n  canvas.addEventListener('wheel',e=>{\n    e.preventDefault();orbit.dist=clamp(orbit.dist*Math.exp(e.deltaY*.001),130,850);updateCamera();\n  },{passive:false});\n\n  \/\/ \u751f\u6210\u6240\u6709\u6ed1\u5757\uff0c\u6570\u503c\u8f93\u5165\u4e0e\u62d6\u52a8\u6761\u53cc\u5411\u540c\u6b65\u3002\n  function slider(group,label,value,min,max,step,onChange){\n    const row=document.createElement('div');row.className='ue-row';\n    const name=document.createElement('label');name.textContent=label;\n    const range=document.createElement('input');range.type='range';\n    range.min=min;range.max=max;range.step=step;range.value=value;\n    const num=document.createElement('input');num.type='number';num.className='ue-number';\n    num.min=min;num.max=max;num.step=step;num.value=value;\n    range.addEventListener('input',()=>{num.value=range.value;onChange(Number(range.value));});\n    num.addEventListener('change',()=>{\n      const next=clamp(Number(num.value)||0,min,max);num.value=next;range.value=next;onChange(next);\n    });\n    row.append(name,range,num);group.appendChild(row);\n  }\n  function controls(container,data,includeScale){\n    container.replaceChildren();\n    const heading=t=>{const el=document.createElement('div');el.className='ue-control-title';el.textContent=t;container.appendChild(el);};\n    heading('Translation \u00b7 cm');\n    ['X','Y','Z'].forEach((axis,i)=>slider(container,axis,data.p[i],includeScale?-65:-150,includeScale?65:150,1,n=>{data.p[i]=n;updateReadout();}));\n    heading('Rotation \u00b7 degree');\n    ['Yaw \/ Z','Pitch \/ Y','Roll \/ X'].forEach((axis,i)=>slider(container,axis,data.r[i],-180,180,1,n=>{data.r[i]=n;updateReadout();}));\n    if(includeScale){\n      heading('Scale');\n      ['X','Y','Z'].forEach((axis,i)=>slider(container,axis,data.s[i],.5,2,.05,n=>{data.s[i]=n;updateReadout();}));\n    }\n  }\n  const selector=$('.ue-bone-select');\n  for(const [id,,,label] of defs){\n    const option=document.createElement('option');option.value=id;option.textContent=label;selector.appendChild(option);\n  }\n  selector.addEventListener('change',()=>selectBone(selector.value));\n  function chain(id){\n    const p=[];while(id){\n      p.unshift(id);const spec=defs.find(d=>d[0]===id);id=spec?.[1];\n    }\n    return p.join(' \u2192 ');\n  }\n  function selectBone(id){\n    if(!nodes[id])return;\n    selected=id;selector.value=id;\n    $('.ue-path').textContent=chain(id);\n    nodes[id].add(selectedAxes);\n    controls($('.ue-bone-controls'),state[id],true);\n    updateReadout();\n  }\n  \/\/ \u5c06 MatrixWorld \u8f6c\u6210 T\/R\/S\uff0c\u7ec4\u4ef6\u7a7a\u95f4\u7531 World * Inverse(Component) \u5f97\u5230\u3002\n  function decomposition(matrix){\n    const p=new THREE.Vector3(),q=new THREE.Quaternion(),s=new THREE.Vector3();\n    matrix.decompose(p,q,s);\n    const e=new THREE.Euler().setFromQuaternion(q,'ZYX');\n    return {p:p.toArray(),r:[e.z\/DEG,-e.y\/DEG,-e.x\/DEG],s:s.toArray()};\n  }\n  function updateReadout(){\n    updateScene();\n    const node=nodes[selected];\n    const local=state[selected];\n    const comp=decomposition(meshRoot.matrixWorld.clone().invert().multiply(node.matrixWorld));\n    const world=decomposition(node.matrixWorld);\n    const box=$('.ue-coordinates');box.replaceChildren();\n    for(const [name,d] of [['Local \u2192 Parent',local],['Component \u2192 Mesh',comp],['World \u2192 Level',world]]){\n      const card=document.createElement('div');card.className='ue-coord-card';\n      const title=document.createElement('div');title.className='ue-coord-title';title.textContent=name;\n      const value=document.createElement('div');value.className='ue-coord-value';\n      value.textContent='T  ('+nice(d.p)+') cm\\nR  ('+nice(d.r)+') \u00b0\\nS  ('+nice(d.s)+')';\n      card.append(title,value);box.appendChild(card);\n    }\n  }\n  controls($('.ue-component-controls'),component,false);selectBone(selected);\n  host.querySelectorAll('[data-preset]').forEach(btn=>btn.addEventListener('click',()=>{\n    if(btn.dataset.preset==='turn'){\n      component.r[0]=90;controls($('.ue-component-controls'),component,false);\n    }\n    if(btn.dataset.preset==='arm'){state.upperarm_r.r[2]=-70;selectBone('upperarm_r');}\n    if(btn.dataset.preset==='bend'){state.spine.r[1]=30;selectBone('spine');}\n    updateReadout();\n  }));\n  $('[data-action=\"reset\"]').addEventListener('click',()=>{\n    component.p=[0,0,0];component.r=[0,0,0];\n    for(const [id,,p] of defs)state[id]={p:[...p],r:[0,0,0],s:[1,1,1]};\n    controls($('.ue-component-controls'),component,false);selectBone('upperarm_r');\n    orbit.theta=.84;orbit.phi=1.05;orbit.dist=370;updateCamera();updateReadout();\n  });\n  host.querySelectorAll('[data-option]').forEach(input=>input.addEventListener('change',()=>{\n    const on=input.checked;\n    if(input.dataset.option==='model')showModel=on;\n    if(input.dataset.option==='skeleton')showSkeleton=on;\n    if(input.dataset.option==='axes')showAxes=on;\n    if(input.dataset.option==='componentAxes')showComponentAxes=on;\n    updateReadout();\n  }));\n  const resize=()=>{\n    const w=Math.max(canvasArea.clientWidth,10),h=Math.max(canvasArea.clientHeight,10);\n    camera.aspect=w\/h;camera.updateProjectionMatrix();renderer.setSize(w,h,false);\n  };\n  if('ResizeObserver' in window)new ResizeObserver(resize).observe(canvasArea);\n  else window.addEventListener('resize',resize);\n  resize();loading.style.display='none';\n  const marker=$('.ue-marker');\n  function frame(){\n    updateScene();\n    const p=wpos(selected).project(camera);\n    if (!(Math.abs(p.z) >= 1)) {\n      marker.style.display='block';\n      marker.style.left=((p.x+1)*.5*canvasArea.clientWidth)+'px';\n      marker.style.top=((1-p.y)*.5*canvasArea.clientHeight)+'px';\n      marker.textContent=selected;\n    }else marker.style.display='none';\n    renderer.render(scene,camera);requestAnimationFrame(frame);\n  }\n  frame();host.dataset.initialized='true';\n  }catch(error){fail('3D \u521d\u59cb\u5316\u51fa\u9519\uff1a'+(error?.message||String(error)),error);}\n})();\n<\/script>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4e2a\u6f14\u793a\u4f53\u73b0\u51fa\uff0c\u9aa8\u9abc\u7684\u672c\u8d28\u5c5e\u6027\u662f<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u76f8\u5bf9\u4e8e\u7236\u9aa8\u9abc\u7684\u4f4d\u79fb\u5c5e\u6027\uff0c\u5e38\u91cf<\/li>\n\n\n\n<li>\u51b3\u5b9a\u5b50\u9aa8\u9abc\u94fe\u7684\u65cb\u8f6c\u5c5e\u6027\uff0c\u53d8\u91cf\uff0c\u8868\u5f81\u9aa8\u9abc\u7684\u8fd0\u52a8<\/li>\n\n\n\n<li>\u81f3\u4e8escale\uff0c\u5728\u5927\u591a\u6570\u4e0d\u9700\u8981\u626d\u66f2\u5f62\u53d8\u7684\u60c5\u51b5\u4e0b\uff0c\u662f\u4e0d\u9700\u8981\u7684\uff0c\u9ed8\u8ba4\u4e3a\uff081\uff0c1\uff0c1\uff09<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Unreal\u7684\u4e16\u754c\u5750\u6807\u7cfb\uff0c\u7ecf\u8fc7SkeletonComponent\u7684\u5750\u6807\u53d8\u6362\uff0c\u8f6c\u53d8\u4e3aComponent\u7a7a\u95f4\u5750\u6807\u3002\u8fd9\u4e2aComponent\u5c40\u90e8\u5750\u6807\u7cfb\uff0c\u4e5f\u5c31\u662f\u89d2\u8272\u81ea\u5df1\u7684\u5c40\u90e8\u5750\u6807\u7cfb\u3002\u89d2\u8272\u8f6c\u8eab\uff0c\u5e73\u79fb\uff0c\u53ea\u9700\u8981\u6539\u53d8SkeletonComponent\u7684\u53d8\u6362\u77e9\u9635<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5927\u591a\u6570\u60c5\u51b5\u4e0b\uff0c\u5b9a\u4e49\u597d\u7684\u9aa8\u9abc\uff0c\u5728\u8fd0\u52a8\u4e2d\u53ea\u6539\u53d8\u65cb\u8f6c\u5c5e\u6027\uff0c\u76f8\u5f53\u4e8e\u4e00\u4e9b\u521a\u4f53\u68cd\u5934\u5c3e\u76f8\u63a5\u8fde\u6210\u7684\u94fe<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Kawaii \u7684\u8bbe\u8ba1<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Kawaii\u7684\u6838\u5fc3\u53ef\u4ee5\u6982\u62ec\u4e3a\uff1a\u7b80\u5316\u7269\u7406\uff0c\u4e8c\u6b21\u5143\u7279\u5316\u3002\u4e0b\u9762\u53ef\u4ee5\u5c55\u5f00\u8bb2\u8bb2\u8fd9\u4e24\u70b9\u4f53\u73b0\u5728\u4ec0\u4e48\u5730\u65b9<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. \u7b80\u5316\u7269\u7406<\/h3>\n\n\n\n<h3 class=\"wp-block-heading\">2. \u4e8c\u6b21\u5143\u7279\u5316<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6b63\u5e38\u60c5\u51b5\u4e0b\uff0c<\/p>\n","protected":false},"excerpt":{"rendered":"<p>wuwa\u4f7f\u7528\u4e00\u4e2a\u81ea\u5b9a\u4e49\u7248\u672c\u7684 kawaii Physics \u6765\u5904\u7406\u89d2\u8272\u8863\u670d\uff0c\u9970\u54c1\u7b49\u7684\u6b21\u7ea7\u8fd0\u52a8\u3002\u5728\u590d\u73b0\u7684\u65f6\u5019\uff0c\u53d1 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8],"tags":[18,40],"class_list":["post-2813","post","type-post","status-publish","format-standard","hentry","category-8","tag-ue","tag-40"],"_links":{"self":[{"href":"https:\/\/ferina.top\/index.php\/wp-json\/wp\/v2\/posts\/2813","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ferina.top\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ferina.top\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ferina.top\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ferina.top\/index.php\/wp-json\/wp\/v2\/comments?post=2813"}],"version-history":[{"count":11,"href":"https:\/\/ferina.top\/index.php\/wp-json\/wp\/v2\/posts\/2813\/revisions"}],"predecessor-version":[{"id":2831,"href":"https:\/\/ferina.top\/index.php\/wp-json\/wp\/v2\/posts\/2813\/revisions\/2831"}],"wp:attachment":[{"href":"https:\/\/ferina.top\/index.php\/wp-json\/wp\/v2\/media?parent=2813"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ferina.top\/index.php\/wp-json\/wp\/v2\/categories?post=2813"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ferina.top\/index.php\/wp-json\/wp\/v2\/tags?post=2813"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}