ThreeJS骨骼示例

html 复制代码
<html lang="zh-CN"><head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>骨骼动画混合演示</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
            font-family: 'Arial', sans-serif;
            overflow: hidden;
        }
        
        #container {
            position: relative;
            width: 100vw;
            height: 100vh;
        }
        
        #controls {
            position: absolute;
            top: 20px;
            right: 20px;
            background: rgba(0, 0, 0, 0.8);
            padding: 20px;
            border-radius: 10px;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.2);
            z-index: 100;
            min-width: 200px;
        }
        
        .control-section {
            margin-bottom: 15px;
        }
        
        .control-section h3 {
            color: #fff;
            margin: 0 0 10px 0;
            font-size: 14px;
            text-transform: uppercase;
            letter-spacing: 1px;
        }
        
        button {
            background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
            border: none;
            padding: 8px 15px;
            margin: 3px;
            border-radius: 5px;
            color: white;
            cursor: pointer;
            font-size: 12px;
            transition: all 0.3s ease;
            min-width: 80px;
        }
        
        button:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
        }
        
        button.active {
            background: linear-gradient(45deg, #f093fb 0%, #f5576c 100%);
            box-shadow: 0 0 20px rgba(240, 147, 251, 0.5);
        }
        
        .slider-container {
            margin: 10px 0;
        }
        
        .slider-container label {
            color: #fff;
            font-size: 12px;
            display: block;
            margin-bottom: 5px;
        }
        
        input[type="range"] {
            width: 100%;
            height: 4px;
            border-radius: 2px;
            background: rgba(255, 255, 255, 0.3);
            outline: none;
            -webkit-appearance: none;
        }
        
        input[type="range"]::-webkit-slider-thumb {
            appearance: none;
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
            cursor: pointer;
        }
        
        #info {
            position: absolute;
            bottom: 20px;
            left: 20px;
            color: rgba(255, 255, 255, 0.8);
            font-size: 12px;
            max-width: 300px;
        }
    </style>
<input type="hidden" id="_o_dbjbempljhcmhlfpfacalomonjpalpko" data-inspect-config="7"><script type="text/javascript" src="chrome-extension://odphnbhiddhdpoccbialllejaajemdio/scripts/inspector.js"></script><link id="fl-highlights-css" rel="stylesheet" type="text/css" href="chrome-extension://dppcnmibpgmagiigcnfdjpnghplibbna/fl-highlights.css"><style id="monica-reading-highlight-style">
        .monica-reading-highlight {
          animation: fadeInOut 1.5s ease-in-out;
        }

        @keyframes fadeInOut {
          0%, 100% { background-color: transparent; }
          30%, 70% { background-color: rgba(2, 118, 255, 0.20); }
        }
      </style><style data-id="immersive-translate-input-injected-css">.immersive-translate-input {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  bottom: 0;
  z-index: 2147483647;
  display: flex;
  justify-content: center;
  align-items: center;
}
.immersive-translate-attach-loading::after {
  content: " ";

  --loading-color: #f78fb6;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  display: block;
  margin: 12px auto;
  position: relative;
  color: white;
  left: -100px;
  box-sizing: border-box;
  animation: immersiveTranslateShadowRolling 1.5s linear infinite;

  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-2000%, -50%);
  z-index: 100;
}

.immersive-translate-loading-spinner {
  vertical-align: middle !important;
  width: 10px !important;
  height: 10px !important;
  display: inline-block !important;
  margin: 0 4px !important;
  border: 2px rgba(221, 244, 255, 0.6) solid !important;
  border-top: 2px rgba(0, 0, 0, 0.375) solid !important;
  border-left: 2px rgba(0, 0, 0, 0.375) solid !important;
  border-radius: 50% !important;
  padding: 0 !important;
  -webkit-animation: immersive-translate-loading-animation 0.6s infinite linear !important;
  animation: immersive-translate-loading-animation 0.6s infinite linear !important;
}

@-webkit-keyframes immersive-translate-loading-animation {
  from {
    -webkit-transform: rotate(0deg);
  }

  to {
    -webkit-transform: rotate(359deg);
  }
}

@keyframes immersive-translate-loading-animation {
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(359deg);
  }
}

.immersive-translate-input-loading {
  --loading-color: #f78fb6;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  display: block;
  margin: 12px auto;
  position: relative;
  color: white;
  left: -100px;
  box-sizing: border-box;
  animation: immersiveTranslateShadowRolling 1.5s linear infinite;
}

@keyframes immersiveTranslateShadowRolling {
  0% {
    box-shadow: 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0),
      0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0);
  }

  12% {
    box-shadow: 100px 0 var(--loading-color), 0px 0 rgba(255, 255, 255, 0),
      0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0);
  }

  25% {
    box-shadow: 110px 0 var(--loading-color), 100px 0 var(--loading-color),
      0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0);
  }

  36% {
    box-shadow: 120px 0 var(--loading-color), 110px 0 var(--loading-color),
      100px 0 var(--loading-color), 0px 0 rgba(255, 255, 255, 0);
  }

  50% {
    box-shadow: 130px 0 var(--loading-color), 120px 0 var(--loading-color),
      110px 0 var(--loading-color), 100px 0 var(--loading-color);
  }

  62% {
    box-shadow: 200px 0 rgba(255, 255, 255, 0), 130px 0 var(--loading-color),
      120px 0 var(--loading-color), 110px 0 var(--loading-color);
  }

  75% {
    box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0),
      130px 0 var(--loading-color), 120px 0 var(--loading-color);
  }

  87% {
    box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0),
      200px 0 rgba(255, 255, 255, 0), 130px 0 var(--loading-color);
  }

  100% {
    box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0),
      200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0);
  }
}

.immersive-translate-toast {
  display: flex;
  position: fixed;
  z-index: 2147483647;
  left: 0;
  right: 0;
  top: 1%;
  width: fit-content;
  padding: 12px 20px;
  margin: auto;
  overflow: auto;
  background: #fef6f9;
  box-shadow: 0px 4px 10px 0px rgba(0, 10, 30, 0.06);
  font-size: 15px;
  border-radius: 8px;
  color: #333;
}

.immersive-translate-toast-content {
  display: flex;
  flex-direction: row;
  align-items: center;
}

.immersive-translate-toast-hidden {
  margin: 0 20px 0 72px;
  text-decoration: underline;
  cursor: pointer;
}

.immersive-translate-toast-close {
  color: #666666;
  font-size: 20px;
  font-weight: bold;
  padding: 0 10px;
  cursor: pointer;
}

@media screen and (max-width: 768px) {
  .immersive-translate-toast {
    top: 0;
    padding: 12px 0px 0 10px;
  }
  .immersive-translate-toast-content {
    flex-direction: column;
    text-align: center;
  }
  .immersive-translate-toast-hidden {
    margin: 10px auto;
  }
}

.immersive-translate-dialog {
  position: fixed;
  z-index: 2147483647;
  left: 0;
  top: 0;
  display: flex;
  width: 300px;
  flex-direction: column;
  align-items: center;
  font-size: 15px;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
  height: fit-content;
  border-radius: 20px;
  background-color: #fff;
}

.immersive-translate-modal {
  display: none;
  position: fixed;
  z-index: 2147483647;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: auto;
  background-color: rgb(0, 0, 0);
  background-color: rgba(0, 0, 0, 0.4);
  font-size: 15px;
}

.immersive-translate-modal-content {
  background-color: #fefefe;
  margin: 10% auto;
  padding: 40px 24px 24px;
  border-radius: 12px;
  width: 350px;
  font-family: system-ui, -apple-system, "Segoe UI", "Roboto", "Ubuntu",
    "Cantarell", "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
    "Segoe UI Symbol", "Noto Color Emoji";
  position: relative;
}

@media screen and (max-width: 768px) {
  .immersive-translate-modal-content {
    margin: 25% auto !important;
  }
}

@media screen and (max-width: 480px) {
  .immersive-translate-modal-content {
    width: 80vw !important;
    margin: 20vh auto !important;
    padding: 20px 12px 12px !important;
  }

  .immersive-translate-modal-title {
    font-size: 14px !important;
  }

  .immersive-translate-modal-body {
    font-size: 13px !important;
    max-height: 60vh !important;
  }

  .immersive-translate-btn {
    font-size: 13px !important;
    padding: 8px 16px !important;
    margin: 0 4px !important;
  }

  .immersive-translate-modal-footer {
    gap: 6px !important;
    margin-top: 16px !important;
  }
}

.immersive-translate-modal .immersive-translate-modal-content-in-input {
  max-width: 500px;
}
.immersive-translate-modal-content-in-input .immersive-translate-modal-body {
  text-align: left;
  max-height: unset;
}

.immersive-translate-modal-title {
  text-align: center;
  font-size: 16px;
  font-weight: 700;
  color: #333333;
}

.immersive-translate-modal-body {
  text-align: center;
  font-size: 14px;
  font-weight: 400;
  color: #333333;
  margin-top: 24px;
}

@media screen and (max-width: 768px) {
  .immersive-translate-modal-body {
    max-height: 250px;
    overflow-y: auto;
  }
}

.immersive-translate-close {
  color: #666666;
  position: absolute;
  right: 16px;
  top: 16px;
  font-size: 20px;
  font-weight: bold;
}

.immersive-translate-close:hover,
.immersive-translate-close:focus {
  text-decoration: none;
  cursor: pointer;
}

.immersive-translate-modal-footer {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 24px;
}

.immersive-translate-btn {
  width: fit-content;
  color: #fff;
  background-color: #ea4c89;
  border: none;
  font-size: 14px;
  margin: 0 8px;
  padding: 9px 30px;
  border-radius: 5px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.immersive-translate-btn-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.immersive-translate-btn:hover {
  background-color: #f082ac;
}
.immersive-translate-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.immersive-translate-btn:disabled:hover {
  background-color: #ea4c89;
}

.immersive-translate-link-btn {
  background-color: transparent;
  color: #ea4c89;
  border: none;
  cursor: pointer;
  height: 30px;
  line-height: 30px;
}

.immersive-translate-cancel-btn {
  /* gray color */
  background-color: rgb(89, 107, 120);
}

.immersive-translate-cancel-btn:hover {
  background-color: hsl(205, 20%, 32%);
}

.immersive-translate-action-btn {
  background-color: transparent;
  color: #ea4c89;
  border: 1px solid #ea4c89;
}

.immersive-translate-btn svg {
  margin-right: 5px;
}

.immersive-translate-link {
  cursor: pointer;
  user-select: none;
  -webkit-user-drag: none;
  text-decoration: none;
  color: #ea4c89;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}

.immersive-translate-primary-link {
  cursor: pointer;
  user-select: none;
  -webkit-user-drag: none;
  text-decoration: none;
  color: #ea4c89;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}

.immersive-translate-modal input[type="radio"] {
  margin: 0 6px;
  cursor: pointer;
}

.immersive-translate-modal label {
  cursor: pointer;
}

.immersive-translate-close-action {
  position: absolute;
  top: 2px;
  right: 0px;
  cursor: pointer;
}

.imt-image-status {
  background-color: rgba(0, 0, 0, 0.5) !important;
  display: flex !important;
  flex-direction: column !important;
  align-items: center !important;
  justify-content: center !important;
  border-radius: 16px !important;
}
.imt-image-status img,
.imt-image-status svg,
.imt-img-loading {
  width: 28px !important;
  height: 28px !important;
  margin: 0 0 8px 0 !important;
  min-height: 28px !important;
  min-width: 28px !important;
  position: relative !important;
}
.imt-img-loading {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAMAAACfWMssAAAAtFBMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////oK74hAAAAPHRSTlMABBMIDyQXHwyBfFdDMSw+OjXCb+5RG51IvV/k0rOqlGRM6KKMhdvNyZBz9MaupmxpWyj437iYd/yJVNZeuUC7AAACt0lEQVRIx53T2XKiUBCA4QYOiyCbiAsuuGBcYtxiYtT3f6/pbqoYHVFO5r+iivpo6DpAWYpqeoFfr9f90DsYAuRSWkFnPO50OgR9PwiCUFcl2GEcx+N/YBh6pvKaefHlUgZd1zVe0NbYcQjGBfzrPE8Xz8aF+71D8gG6DHFPpc4a7xFiCDuhaWgKgGIJQ3d5IMGDrpS4S5KgpIm+en9f6PlAhKby4JwEIxlYJV9h5k5nee9GoxHJ2IDSNB0dwdad1NAxDJ/uXDHYmebdk4PdbkS58CIVHdYSUHTYYRWOJblWSyu2lmy3KNFVJNBhxcuGW4YBVCbYGRZwIooipHsNqjM4FbgOQqQqSKQQU9V8xmi1QlgHqQQ6DDBvRUVCDirs+EzGDGOQTCATgtYTnbCVLgsVgRE0T1QE0qHCFAht2z6dLvJQs3Lo2FQoDxWNUiBhaP4eRgwNkI+dAjVOA/kUrIDwf3CG8NfNOE0eiFotSuo+rBiq8tD9oY4Qzc6YJw99hl1wzpQvD7ef2M8QgnOGJfJw+EltQc+oX2yn907QB22WZcvlUpd143dqQu+8pCJZuGE4xCuPXJqqcs5sNpsI93Rmzym1k4Npk+oD1SH3/a3LOK/JpUBpWfqNySxWzCfNCUITuDG5dtuphrUJ1myeIE9bIsPiKrfqTai5WZxbhtNphYx6GEIHihyGFTI69lje/rxajdh0s0msZ0zYxyPLhYCb1CyHm9Qsd2H37Y3lugVwL9kNh8Ot8cha6fUNQ8nuXi5z9/ExsAO4zQrb/ev1yrCB7lGyQzgYDGuxq1toDN/JGvN+HyWNHKB7zEoK+PX11e12G431erGYzwmytAWU56fkMHY5JJnDRR2eZji3AwtIcrEV8Cojat/BdQ7XOwGV1e1hDjGGjXbdArm8uJZtCH5MbcctVX8A1WpqumJHwckAAAAASUVORK5CYII=");
  background-size: 28px 28px;
  animation: image-loading-rotate 1s linear infinite !important;
}

.imt-image-status span {
  color: var(--bg-2, #fff) !important;
  font-size: 14px !important;
  line-height: 14px !important;
  font-weight: 500 !important;
  font-family: "PingFang SC", Arial, sans-serif !important;
}

.imt-primary-button {
  display: flex;
  padding: 12px 80px;
  justify-content: center;
  align-items: center;
  gap: 8px;
  border-radius: 8px;
  background: #ea4c89;
  color: #fff;
  font-size: 16px;
  font-style: normal;
  font-weight: 700;
  line-height: 24px;
  border: none;
  cursor: pointer;
}

.imt-retry-text {
  color:  #999;
  text-align: center;
  font-size: 14px;
  font-style: normal;
  font-weight: 400;
  line-height: 21px;
  cursor: pointer;
}

.imt-action-container {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.imt-modal-content-text {
  text-align: left;
  color:  #333;
  font-size: 16px;
  font-weight: 400;
  line-height: 24px;
}

@keyframes image-loading-rotate {
  from {
    transform: rotate(360deg);
  }
  to {
    transform: rotate(0deg);
  }
}
</style></head>
<body monica-id="ofpnmcalabcbjgholdjcjblkibolbppb" monica-version="7.9.7">
    <div id="container">
        <div id="controls">
            <div class="control-section">
                <h3>动作切换</h3>
                <button id="idle" class="active">站立</button>
                <button id="walk">走路</button>
                <button id="run">奔跑</button>
                <button id="jump">跳跃</button>
            </div>
            
            <div class="control-section">
                <h3>动画设置</h3>
                <div class="slider-container">
                    <label>混合速度: <span id="blendSpeedValue">2.0</span></label>
                    <input type="range" id="blendSpeed" min="0.5" max="5.0" step="0.1" value="2.0">
                </div>
                <div class="slider-container">
                    <label>动画速度: <span id="animSpeedValue">1.0</span></label>
                    <input type="range" id="animSpeed" min="0.1" max="3.0" step="0.1" value="1.0">
                </div>
            </div>
            
            <div class="control-section">
                <button id="autoMode">自动模式</button>
            </div>
        </div>
        
        <div id="info">
            <strong>骨骼动画混合演示</strong><br>
            点击右侧按钮切换动作,观察不同动画之间的平滑过渡。<br>
            • 蓝色关节:骨骼系统<br>
            • 绿色线条:骨骼连接<br>
            • 红色方块:蒙皮网格
        </div>
    <canvas width="753" height="672" style="display: block; width: 753px; height: 672px;"></canvas></div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script>
        // 全局变量
        let scene, camera, renderer, character;
        let currentAction = 'idle';
        let isAutoMode = false;
        let autoModeTimer = 0;
        let blendSpeed = 2.0;
        let animationSpeed = 1.0;
        
        // 动画状态
        const animations = {
            idle: { weight: 1.0, speed: 1.0 },
            walk: { weight: 0.0, speed: 1.5 },
            run: { weight: 0.0, speed: 2.0 },
            jump: { weight: 0.0, speed: 1.2 }
        };
        
        // 简化的骨骼系统
        class SimpleSkeleton {
            constructor() {
                this.bones = [];
                this.joints = [];
                this.meshes = [];
                this.time = 0;
                this.createSkeleton();
            }
            
            createSkeleton() {
                // 创建骨骼结构(简化的人形)
                const boneStructure = [
                    { name: 'root', pos: [0, 0, 0], parent: null },
                    { name: 'spine', pos: [0, 1, 0], parent: 'root' },
                    { name: 'chest', pos: [0, 2, 0], parent: 'spine' },
                    { name: 'neck', pos: [0, 2.8, 0], parent: 'chest' },
                    { name: 'head', pos: [0, 3.5, 0], parent: 'neck' },
                    
                    { name: 'leftShoulder', pos: [-0.8, 2.5, 0], parent: 'chest' },
                    { name: 'leftArm', pos: [-1.5, 2.5, 0], parent: 'leftShoulder' },
                    { name: 'leftForearm', pos: [-2.2, 2.5, 0], parent: 'leftArm' },
                    
                    { name: 'rightShoulder', pos: [0.8, 2.5, 0], parent: 'chest' },
                    { name: 'rightArm', pos: [1.5, 2.5, 0], parent: 'rightShoulder' },
                    { name: 'rightForearm', pos: [2.2, 2.5, 0], parent: 'rightArm' },
                    
                    { name: 'leftHip', pos: [-0.3, 0, 0], parent: 'root' },
                    { name: 'leftThigh', pos: [-0.3, -0.8, 0], parent: 'leftHip' },
                    { name: 'leftCalf', pos: [-0.3, -1.6, 0], parent: 'leftThigh' },
                    { name: 'leftFoot', pos: [-0.3, -2.2, 0.2], parent: 'leftCalf' },
                    
                    { name: 'rightHip', pos: [0.3, 0, 0], parent: 'root' },
                    { name: 'rightThigh', pos: [0.3, -0.8, 0], parent: 'rightHip' },
                    { name: 'rightCalf', pos: [0.3, -1.6, 0], parent: 'rightThigh' },
                    { name: 'rightFoot', pos: [0.3, -2.2, 0.2], parent: 'rightCalf' }
                ];
                
                // 创建骨骼对象
                const boneMap = {};
                boneStructure.forEach(bone => {
                    const boneObject = new THREE.Object3D();
                    boneObject.position.set(bone.pos[0], bone.pos[1], bone.pos[2]);
                    boneObject.name = bone.name;
                    boneMap[bone.name] = boneObject;
                    this.bones.push(boneObject);
                    
                    // 创建关节可视化
                    const jointGeometry = new THREE.SphereGeometry(0.08, 8, 8);
                    const jointMaterial = new THREE.MeshBasicMaterial({ color: 0x4488ff });
                    const joint = new THREE.Mesh(jointGeometry, jointMaterial);
                    boneObject.add(joint);
                    this.joints.push(joint);
                    
                    scene.add(boneObject);
                });
                
                // 建立父子关系
                boneStructure.forEach(bone => {
                    if (bone.parent) {
                        const parentBone = boneMap[bone.parent];
                        const childBone = boneMap[bone.name];
                        parentBone.add(childBone);
                        
                        // 创建骨骼连接线
                        const lineGeometry = new THREE.BufferGeometry();
                        const lineVertices = new Float32Array([
                            0, 0, 0,
                            childBone.position.x - parentBone.position.x,
                            childBone.position.y - parentBone.position.y,
                            childBone.position.z - parentBone.position.z
                        ]);
                        lineGeometry.setAttribute('position', new THREE.BufferAttribute(lineVertices, 3));
                        const lineMaterial = new THREE.LineBasicMaterial({ color: 0x00ff88 });
                        const line = new THREE.Line(lineGeometry, lineMaterial);
                        parentBone.add(line);
                    }
                });
                
                this.boneMap = boneMap;
                
                // 创建简单的蒙皮网格
                this.createSkinMesh();
            }
            
            createSkinMesh() {
                // 身体部位的简单网格
                const bodyParts = [
                    { name: 'torso', bone: 'chest', size: [0.8, 1.2, 0.4], offset: [0, -0.3, 0] },
                    { name: 'head', bone: 'head', size: [0.4, 0.4, 0.4], offset: [0, 0, 0] },
                    { name: 'leftArm', bone: 'leftArm', size: [0.6, 0.15, 0.15], offset: [-0.3, 0, 0] },
                    { name: 'rightArm', bone: 'rightArm', size: [0.6, 0.15, 0.15], offset: [0.3, 0, 0] },
                    { name: 'leftThigh', bone: 'leftThigh', size: [0.2, 0.7, 0.2], offset: [0, -0.35, 0] },
                    { name: 'rightThigh', bone: 'rightThigh', size: [0.2, 0.7, 0.2], offset: [0, -0.35, 0] },
                    { name: 'leftCalf', bone: 'leftCalf', size: [0.15, 0.6, 0.15], offset: [0, -0.3, 0] },
                    { name: 'rightCalf', bone: 'rightCalf', size: [0.15, 0.6, 0.15], offset: [0, -0.3, 0] }
                ];
                
                bodyParts.forEach(part => {
                    const geometry = new THREE.BoxGeometry(part.size[0], part.size[1], part.size[2]);
                    const material = new THREE.MeshPhongMaterial({ 
                        color: 0xff4444, 
                        transparent: true, 
                        opacity: 0.7 
                    });
                    const mesh = new THREE.Mesh(geometry, material);
                    mesh.position.set(part.offset[0], part.offset[1], part.offset[2]);
                    
                    if (this.boneMap[part.bone]) {
                        this.boneMap[part.bone].add(mesh);
                        this.meshes.push(mesh);
                    }
                });
            }
            
            // 动画函数
            animate(action, time, weight = 1.0) {
                const t = time * animationSpeed;
                
                switch(action) {
                    case 'idle':
                        this.animateIdle(t, weight);
                        break;
                    case 'walk':
                        this.animateWalk(t, weight);
                        break;
                    case 'run':
                        this.animateRun(t, weight);
                        break;
                    case 'jump':
                        this.animateJump(t, weight);
                        break;
                }
            }
            
            animateIdle(time, weight) {
                const breathe = Math.sin(time * 2) * 0.02 * weight;
                const sway = Math.sin(time * 0.8) * 0.01 * weight;
                
                if (this.boneMap['chest']) {
                    this.boneMap['chest'].rotation.z = sway;
                    this.boneMap['chest'].position.y = 2 + breathe;
                }
                
                if (this.boneMap['head']) {
                    this.boneMap['head'].rotation.x = Math.sin(time * 1.5) * 0.05 * weight;
                }
            }
            
            animateWalk(time, weight) {
                const walkCycle = time * 3;
                const armSwing = Math.sin(walkCycle) * 0.3 * weight;
                const legSwing = Math.sin(walkCycle) * 0.4 * weight;
                const bodyBob = Math.abs(Math.sin(walkCycle * 2)) * 0.05 * weight;
                
                // 手臂摆动
                if (this.boneMap['leftArm']) {
                    this.boneMap['leftArm'].rotation.x = armSwing;
                }
                if (this.boneMap['rightArm']) {
                    this.boneMap['rightArm'].rotation.x = -armSwing;
                }
                
                // 腿部运动
                if (this.boneMap['leftThigh']) {
                    this.boneMap['leftThigh'].rotation.x = legSwing;
                }
                if (this.boneMap['rightThigh']) {
                    this.boneMap['rightThigh'].rotation.x = -legSwing;
                }
                
                // 身体上下摆动
                if (this.boneMap['root']) {
                    this.boneMap['root'].position.y = bodyBob;
                }
            }
            
            animateRun(time, weight) {
                const runCycle = time * 6;
                const armSwing = Math.sin(runCycle) * 0.6 * weight;
                const legSwing = Math.sin(runCycle) * 0.8 * weight;
                const bodyBob = Math.abs(Math.sin(runCycle * 2)) * 0.15 * weight;
                const bodyLean = Math.sin(runCycle) * 0.1 * weight;
                
                // 更大的手臂摆动
                if (this.boneMap['leftArm']) {
                    this.boneMap['leftArm'].rotation.x = armSwing;
                }
                if (this.boneMap['rightArm']) {
                    this.boneMap['rightArm'].rotation.x = -armSwing;
                }
                
                // 更大的腿部运动
                if (this.boneMap['leftThigh']) {
                    this.boneMap['leftThigh'].rotation.x = legSwing;
                }
                if (this.boneMap['rightThigh']) {
                    this.boneMap['rightThigh'].rotation.x = -legSwing;
                }
                
                // 身体前倾和上下摆动
                if (this.boneMap['root']) {
                    this.boneMap['root'].position.y = bodyBob;
                }
                if (this.boneMap['chest']) {
                    this.boneMap['chest'].rotation.x = bodyLean * 0.3;
                }
            }
            
            animateJump(time, weight) {
                const jumpPhase = (time % 2) / 2; // 2秒一个循环
                let jumpY = 0;
                let armPose = 0;
                let legBend = 0;
                
                if (jumpPhase < 0.3) { // 准备阶段
                    const t = jumpPhase / 0.3;
                    jumpY = -0.2 * t * weight;
                    legBend = 0.5 * t * weight;
                    armPose = -0.5 * t * weight;
                } else if (jumpPhase < 0.7) { // 跳跃阶段
                    const t = (jumpPhase - 0.3) / 0.4;
                    const arc = Math.sin(t * Math.PI);
                    jumpY = (arc * 1.5 - 0.2) * weight;
                    legBend = (0.5 - arc * 0.3) * weight;
                    armPose = (arc * 1.2 - 0.5) * weight;
                } else { // 着陆阶段
                    const t = (jumpPhase - 0.7) / 0.3;
                    jumpY = -0.1 * (1 - t) * weight;
                    legBend = 0.2 * (1 - t) * weight;
                    armPose = 0.7 * (1 - t) * weight;
                }
                
                if (this.boneMap['root']) {
                    this.boneMap['root'].position.y = jumpY;
                }
                
                if (this.boneMap['leftArm']) {
                    this.boneMap['leftArm'].rotation.x = armPose;
                }
                if (this.boneMap['rightArm']) {
                    this.boneMap['rightArm'].rotation.x = armPose;
                }
                
                if (this.boneMap['leftThigh']) {
                    this.boneMap['leftThigh'].rotation.x = -legBend;
                }
                if (this.boneMap['rightThigh']) {
                    this.boneMap['rightThigh'].rotation.x = -legBend;
                }
            }
            
            update(deltaTime) {
                this.time += deltaTime;
                
                // 更新动画权重(混合)
                for (let action in animations) {
                    const anim = animations[action];
                    if (action === currentAction) {
                        anim.weight = Math.min(1.0, anim.weight + deltaTime * blendSpeed);
                    } else {
                        anim.weight = Math.max(0.0, anim.weight - deltaTime * blendSpeed);
                    }
                }
                
                // 清除之前的变换
                this.resetBones();
                
                // 应用混合动画
                for (let action in animations) {
                    const anim = animations[action];
                    if (anim.weight > 0.001) {
                        this.animate(action, this.time * anim.speed, anim.weight);
                    }
                }
            }
            
            resetBones() {
                // 重置关键骨骼的变换
                const resetBones = ['root', 'chest', 'head', 'leftArm', 'rightArm', 'leftThigh', 'rightThigh'];
                resetBones.forEach(boneName => {
                    if (this.boneMap[boneName]) {
                        this.boneMap[boneName].rotation.set(0, 0, 0);
                        if (boneName === 'root') {
                            this.boneMap[boneName].position.set(0, 0, 0);
                        } else if (boneName === 'chest') {
                            this.boneMap[boneName].position.set(0, 2, 0);
                        }
                    }
                });
            }
        }
        
        // 初始化场景
        function init() {
            // 创建场景
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x222244);
            
            // 创建摄像机
            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.set(5, 2, 5);
            camera.lookAt(0, 1, 0);
            
            // 创建渲染器
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.shadowMap.enabled = true;
            renderer.shadowMap.type = THREE.PCFSoftShadowMap;
            document.getElementById('container').appendChild(renderer.domElement);
            
            // 添加灯光
            const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
            scene.add(ambientLight);
            
            const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
            directionalLight.position.set(5, 10, 5);
            directionalLight.castShadow = true;
            directionalLight.shadow.mapSize.width = 1024;
            directionalLight.shadow.mapSize.height = 1024;
            scene.add(directionalLight);
            
            // 添加地面
            const groundGeometry = new THREE.PlaneGeometry(20, 20);
            const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x336633 });
            const ground = new THREE.Mesh(groundGeometry, groundMaterial);
            ground.rotation.x = -Math.PI / 2;
            ground.position.y = -2.5;
            ground.receiveShadow = true;
            scene.add(ground);
            
            // 创建角色
            character = new SimpleSkeleton();
            
            // 设置控件
            setupControls();
            
            // 开始动画循环
            animate();
        }
        
        function setupControls() {
            // 动作按钮
            const actionButtons = ['idle', 'walk', 'run', 'jump'];
            actionButtons.forEach(action => {
                const button = document.getElementById(action);
                button.addEventListener('click', () => {
                    if (!isAutoMode) {
                        setAction(action);
                        updateActiveButton(action);
                    }
                });
            });
            
            // 滑块控制
            const blendSpeedSlider = document.getElementById('blendSpeed');
            const blendSpeedValue = document.getElementById('blendSpeedValue');
            blendSpeedSlider.addEventListener('input', (e) => {
                blendSpeed = parseFloat(e.target.value);
                blendSpeedValue.textContent = blendSpeed.toFixed(1);
            });
            
            const animSpeedSlider = document.getElementById('animSpeed');
            const animSpeedValue = document.getElementById('animSpeedValue');
            animSpeedSlider.addEventListener('input', (e) => {
                animationSpeed = parseFloat(e.target.value);
                animSpeedValue.textContent = animationSpeed.toFixed(1);
            });
            
            // 自动模式
            const autoButton = document.getElementById('autoMode');
            autoButton.addEventListener('click', () => {
                isAutoMode = !isAutoMode;
                autoButton.textContent = isAutoMode ? '手动模式' : '自动模式';
                autoButton.classList.toggle('active');
                if (!isAutoMode) {
                    updateActiveButton(currentAction);
                }
            });
        }
        
        function setAction(action) {
            currentAction = action;
        }
        
        function updateActiveButton(action) {
            document.querySelectorAll('#controls button').forEach(btn => {
                btn.classList.remove('active');
            });
            document.getElementById(action).classList.add('active');
        }
        
        let lastTime = 0;
        function animate() {
            requestAnimationFrame(animate);
            
            const currentTime = performance.now() * 0.001;
            const deltaTime = currentTime - lastTime;
            lastTime = currentTime;
            
            // 自动模式切换动作
            if (isAutoMode) {
                autoModeTimer += deltaTime;
                if (autoModeTimer > 3) { // 每3秒切换一次动作
                    const actions = ['idle', 'walk', 'run', 'jump'];
                    const currentIndex = actions.indexOf(currentAction);
                    const nextAction = actions[(currentIndex + 1) % actions.length];
                    setAction(nextAction);
                    autoModeTimer = 0;
                }
            }
            
            // 更新角色动画
            if (character) {
                character.update(deltaTime);
            }
            
            // 摄像机环绕
            const time = currentTime * 0.3;
            camera.position.x = Math.cos(time) * 6;
            camera.position.z = Math.sin(time) * 6;
            camera.position.y = 3;
            camera.lookAt(0, 1, 0);
            
            renderer.render(scene, camera);
        }
        
        // 窗口大小调整
        window.addEventListener('resize', () => {
            const container = document.getElementById('container');
            const containerWidth = container.offsetWidth;
            const containerHeight = container.offsetHeight;
            
            camera.aspect = containerWidth / containerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(containerWidth, containerHeight);
        });
        
        // 启动应用
        init();
    </script>

<div id="monica-content-root" class="monica-widget" style="pointer-events: auto;"></div></body><div id="immersive-translate-popup" style="all: initial"></div></html>

需要全屏演示的同学可以通过**骨骼动画混合演示**查看。

相关推荐
jason_yang10 小时前
基于BEM规范实现ElementPlus组件
css·scss
笑鸿的学习笔记1 天前
JavaScript笔记之JS 和 HTML5 的关系
javascript·笔记·html5
@大迁世界1 天前
用 popover=“hint“ 打造友好的 HTML 提示:一招让界面更“懂人”
开发语言·前端·javascript·css·html
伍哥的传说1 天前
Tailwind CSS v4 终极指南:体验 Rust 驱动的闪电般性能与现代化 CSS 工作流
前端·css·rust·tailwindcss·tailwind css v4·lightning css·utility-first
拜无忧1 天前
前端,用SVG 模仿毛笔写字绘画,defs,filter
前端·css·svg
java水泥工1 天前
基于Echarts+HTML5可视化数据大屏展示-学生综合成绩评价系统大屏
spring boot·echarts·html5
代码小学僧1 天前
🎉 在 Tailwind 中愉快的使用 Antd Design 色彩
前端·css·react.js
ssshooter1 天前
复习 CSS Flex 和 Grid 布局
前端·css·html
Thetimezipsby1 天前
基于Taro4打造的一款最新版微信小程序、H5的多端开发简单模板
前端·javascript·微信小程序·typescript·html5·taro