🎮ArcadeLab

OC 猫猫 · 3D 预览

by TurboKoala83
442 lines13.5 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>OC 猫猫 · 3D 预览</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box;}
  html,body{width:100%;height:100%;overflow:hidden;background:#2a3240;
    font-family:-apple-system,"PingFang SC",sans-serif;
    -webkit-user-select:none;user-select:none;
    -webkit-tap-highlight-color:transparent;touch-action:none;}
  #c{display:block;width:100%;height:100%;}
  #hint{position:fixed;left:0;right:0;bottom:26px;text-align:center;
    color:rgba(255,255,255,.45);font-size:12px;letter-spacing:2px;
    pointer-events:none;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hint">拖动旋转 · 双指缩放</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function(){
'use strict';

/* ================= 渲染器 / 场景 / 相机 ================= */
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({canvas, antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x2a3240);

const camera = new THREE.PerspectiveCamera(42, innerWidth/innerHeight, 0.1, 100);

/* ================= 灯光 ================= */
scene.add(new THREE.HemisphereLight(0xdfeaff, 0x2a2a30, 1.0));

const key = new THREE.DirectionalLight(0xfff0d8, 1.15);
key.position.set(4, 8, 5);
key.castShadow = true;
key.shadow.mapSize.set(1024, 1024);
key.shadow.camera.left = -5;
key.shadow.camera.right = 5;
key.shadow.camera.top = 5;
key.shadow.camera.bottom = -5;
scene.add(key);

const rim = new THREE.DirectionalLight(0x88bbff, 0.45);
rim.position.set(-5, 4, -4);
scene.add(rim);

/* ================= 地面(软阴影盘) ================= */
const groundGeo = new THREE.CircleGeometry(6, 48);
groundGeo.rotateX(-Math.PI/2);
const ground = new THREE.Mesh(groundGeo, new THREE.MeshLambertMaterial({color:0x38424f}));
ground.position.y = -0.01;
ground.receiveShadow = true;
scene.add(ground);

/* ============================================================
   OC 猫猫建模
   配色取自参考图:橘色虎斑 + 绿眼 + 浅色胸口/爪/尾尖
   ============================================================ */
function createCat(){
  const g = new THREE.Group();

  /* ---- 颜色 ---- */
  const C_MAIN  = 0xE69A4E;   // 主体橘
  const C_LIGHT = 0xF6DEBE;   // 胸口/口鼻/爪/尾尖
  const C_DARK  = 0xB0682F;   // 深橘,用于斑纹/耳内
  const C_LINE  = 0x3A2318;   // 深褐,鼻子/嘴线
  const C_EYE   = 0x9BF25B;   // 亮绿

  /* ---- 材质 ---- */
  const matMain  = new THREE.MeshLambertMaterial({color: C_MAIN});
  const matLight = new THREE.MeshLambertMaterial({color: C_LIGHT});
  const matDark  = new THREE.MeshLambertMaterial({color: C_DARK});
  const matLine  = new THREE.MeshLambertMaterial({color: C_LINE});
  const matEyeW  = new THREE.MeshBasicMaterial({color: 0xFFFFFF});
  const matEyeG  = new THREE.MeshBasicMaterial({color: C_EYE});
  const matEyeB  = new THREE.MeshBasicMaterial({color: 0x101014});

  /* ============ 躯干 ============ */
  const body = new THREE.Mesh(new THREE.SphereGeometry(0.55, 18, 14), matMain);
  body.scale.set(1.0, 0.92, 1.45);
  body.position.set(0, 0.86, -0.02);
  body.castShadow = true;
  g.add(body);

  // 后臀(让身体后段更饱满)
  const hip = new THREE.Mesh(new THREE.SphereGeometry(0.5, 14, 11), matMain);
  hip.scale.set(1.0, 0.95, 1.0);
  hip.position.set(0, 0.88, -0.5);
  hip.castShadow = true;
  g.add(hip);

  // 前胸浅色
  const chest = new THREE.Mesh(new THREE.SphereGeometry(0.42, 14, 11), matLight);
  chest.scale.set(0.95, 0.9, 0.85);
  chest.position.set(0, 0.66, 0.44);
  g.add(chest);

  /* ============ 头部 ============ */
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.42, 18, 14), matMain);
  head.scale.set(1.18, 1.0, 1.0);
  head.position.set(0, 1.26, 0.76);
  head.castShadow = true;
  g.add(head);

  // 脸颊毛簇(每侧 3 根,斜向外)
  [-1, 1].forEach(function(sx){
    for (let i = 0; i < 3; i++){
      const spike = new THREE.Mesh(new THREE.ConeGeometry(0.075, 0.28, 5), matMain);
      spike.rotation.z = -sx * (Math.PI/2 + 0.18);
      spike.rotation.y =  0.15 * sx;
      spike.position.set(0.42*sx, 1.28 - i*0.10, 0.78);
      g.add(spike);
    }
  });

  // 口鼻
  const muzzle = new THREE.Mesh(new THREE.SphereGeometry(0.22, 14, 11), matLight);
  muzzle.scale.set(1.18, 0.86, 1.0);
  muzzle.position.set(0, 1.13, 1.03);
  g.add(muzzle);

  // 鼻子(倒三角感)
  const nose = new THREE.Mesh(new THREE.ConeGeometry(0.065, 0.075, 4), matLine);
  nose.rotation.x = Math.PI;
  nose.rotation.y = Math.PI/4;
  nose.position.set(0, 1.19, 1.20);
  g.add(nose);

  // 嘴线(两条小弧)
  const mouthGeo = new THREE.TorusGeometry(0.075, 0.011, 4, 10, Math.PI);
  [-1, 1].forEach(function(sx){
    const m = new THREE.Mesh(mouthGeo, matLine);
    m.rotation.z = Math.PI/2;
    m.rotation.y = Math.PI;
    m.position.set(0.06*sx, 1.08, 1.16);
    g.add(m);
  });

  /* ============ 眼睛(倒三角凶狠眼型) ============ */
  [-1, 1].forEach(function(sx){
    // 眼白
    const eyeW = new THREE.Mesh(new THREE.SphereGeometry(0.105, 12, 9), matEyeW);
    eyeW.scale.set(1.0, 0.82, 0.42);
    eyeW.position.set(0.19*sx, 1.36, 1.04);
    eyeW.rotation.z = 0.28 * sx;
    g.add(eyeW);

    // 绿色虹膜
    const eyeG = new THREE.Mesh(new THREE.SphereGeometry(0.082, 12, 9), matEyeG);
    eyeG.scale.set(0.95, 0.92, 0.42);
    eyeG.position.set(0.19*sx, 1.36, 1.07);
    eyeG.rotation.z = 0.28 * sx;
    g.add(eyeG);

    // 竖瞳
    const pupil = new THREE.Mesh(new THREE.SphereGeometry(0.038, 8, 6), matEyeB);
    pupil.scale.set(0.5, 1.1, 0.4);
    pupil.position.set(0.19*sx, 1.36, 1.10);
    g.add(pupil);

    // 高光
    const spark = new THREE.Mesh(new THREE.SphereGeometry(0.02, 6, 5), matEyeW);
    spark.position.set(0.155*sx, 1.395, 1.115);
    g.add(spark);
  });

  /* ============ 耳朵 ============ */
  [-1, 1].forEach(function(sx){
    const ear = new THREE.Mesh(new THREE.ConeGeometry(0.19, 0.5, 6), matMain);
    ear.position.set(0.26*sx, 1.72, 0.68);
    ear.rotation.z = -0.2 * sx;
    ear.rotation.x = -0.12;
    ear.castShadow = true;
    g.add(ear);

    const inner = new THREE.Mesh(new THREE.ConeGeometry(0.105, 0.3, 6), matDark);
    inner.position.set(0.26*sx, 1.70, 0.72);
    inner.rotation.z = -0.2 * sx;
    inner.rotation.x = -0.12;
    g.add(inner);

    // 耳尖簇毛
    const tuft = new THREE.Mesh(new THREE.ConeGeometry(0.055, 0.16, 5), matDark);
    tuft.position.set(0.26*sx, 1.98, 0.68);
    tuft.rotation.z = -0.24 * sx;
    tuft.rotation.x = -0.12;
    g.add(tuft);
  });

  /* ============ 额头 M 形虎斑纹 ============ */
  [-1, 1].forEach(function(sx){
    for (let i = 0; i < 3; i++){
      const s = new THREE.Mesh(
        new THREE.CylinderGeometry(0.017, 0.017, 0.18, 4),
        matDark
      );
      s.rotation.x = Math.PI/2;
      s.rotation.z = -0.4 * sx;
      s.position.set(0.11*sx, 1.56 - i*0.025, 0.90 - i*0.075);
      g.add(s);
    }
  });

  /* ============ 四肢 ============ */
  const legGeo = new THREE.CylinderGeometry(0.115, 0.095, 0.66, 8);
  legGeo.translate(0, -0.33, 0);
  const pawGeo = new THREE.SphereGeometry(0.125, 10, 8);

  const legs = [];
  const legPos = [
    [ 0.32,  0.48],
    [-0.32,  0.48],
    [ 0.34, -0.48],
    [-0.34, -0.48]
  ];

  legPos.forEach(function(p){
    const leg = new THREE.Mesh(legGeo, matMain);
    leg.position.set(p[0], 0.72, p[1]);
    leg.castShadow = true;

    // 腿上的环纹
    const stripe = new THREE.Mesh(
      new THREE.CylinderGeometry(0.118, 0.118, 0.055, 8),
      matDark
    );
    stripe.position.y = -0.16;
    leg.add(stripe);

    // 浅色爪
    const paw = new THREE.Mesh(pawGeo, matLight);
    paw.scale.set(1.0, 0.72, 1.3);
    paw.position.set(0, -0.66, 0.03);
    leg.add(paw);

    g.add(leg);
    legs.push(leg);
  });

  /* ============ 尾巴(圆柱分段,向后上方弯曲) ============ */
  const tailSegs = [
    { len: 0.28, r0: 0.145, r1: 0.125, ang: -1.15 },
    { len: 0.34, r0: 0.125, r1: 0.105, ang:  0.70 },
    { len: 0.34, r0: 0.105, r1: 0.088, ang:  0.60 },
    { len: 0.30, r0: 0.088, r1: 0.072, ang:  0.45 },
    { len: 0.26, r0: 0.072, r1: 0.060, ang:  0.30 }
  ];

  const tailRoot = new THREE.Group();
  tailRoot.position.set(0, 1.0, -0.85);
  g.add(tailRoot);

  const tailPivots = [];
  let attach = tailRoot;

  tailSegs.forEach(function(s, i){
    const pivot = new THREE.Group();
    if (i > 0) pivot.position.y = tailSegs[i-1].len;
    pivot.rotation.x = s.ang;

    const isTip = (i === tailSegs.length - 1);

    const geo = new THREE.CylinderGeometry(s.r1, s.r0, s.len, 9);
    geo.translate(0, s.len/2, 0);
    const mesh = new THREE.Mesh(geo, isTip ? matLight : matMain);
    mesh.castShadow = true;
    pivot.add(mesh);

    // 深色环纹(尾尖前两段不画)
    if (i < tailSegs.length - 1 && i % 1 === 0){
      const ring = new THREE.Mesh(
        new THREE.CylinderGeometry(s.r0*1.03, s.r0*1.03, 0.055, 9),
        matDark
      );
      ring.position.y = s.len * 0.55;
      pivot.add(ring);
    }

    // 尾尖圆头
    if (isTip){
      const cap = new THREE.Mesh(new THREE.SphereGeometry(s.r1, 9, 7), matLight);
      cap.position.y = s.len;
      pivot.add(cap);
    }

    attach.add(pivot);
    attach = pivot;
    tailPivots.push(pivot);
  });

  /* ============ 躯干侧面的螺旋虎斑 ============ */
  [-1, 1].forEach(function(sx){
    const spiral = new THREE.Mesh(
      new THREE.TorusGeometry(0.155, 0.028, 5, 18),
      matDark
    );
    spiral.scale.set(1, 1, 0.22);
    spiral.rotation.y = Math.PI/2;
    spiral.position.set(0.5*sx, 0.9, 0.0);
    g.add(spiral);
  });

  /* ============ 背部条纹 ============ */
  for (let i = 0; i < 4; i++){
    const stripe = new THREE.Mesh(
      new THREE.CylinderGeometry(0.024, 0.024, 0.3, 4),
      matDark
    );
    stripe.rotation.x = Math.PI/2;
    stripe.rotation.z = 0.4;
    stripe.position.set(0, 1.2 - i*0.02, -0.12 - i*0.17);
    g.add(stripe);
  }

  return { group: g, legs: legs, tail: tailPivots };
}

/* ================= 实例化 ================= */
const cat = createCat();
cat.group.position.set(0, 0, 0);
scene.add(cat.group);

/* ================= 拖动旋转 / 双指缩放 ================= */
let camDist = 6.2;
let camYaw  = Math.PI * 0.22;   // 水平
let camPitch= 0.28;             // 俯仰
let dragging = false;
let lastX = 0, lastY = 0;
let pinchStart = 0, pinchDist0 = 0;

function updateCamera(){
  const cx = Math.sin(camYaw) * Math.cos(camPitch) * camDist;
  const cy = Math.sin(camPitch) * camDist;
  const cz = Math.cos(camYaw) * Math.cos(camPitch) * camDist;
  camera.position.set(cx, cy + 1.05, cz);
  camera.lookAt(0, 1.05, 0);
}
updateCamera();

canvas.addEventListener('touchstart', function(e){
  if (e.touches.length === 1){
    dragging = true;
    lastX = e.touches[0].clientX;
    lastY = e.touches[0].clientY;
  } else if (e.touches.length === 2){
    dragging = false;
    pinchDist0 = Math.hypot(
      e.touches[0].clientX - e.touches[1].clientX,
      e.touches[0].clientY - e.touches[1].clientY
    );
    pinchStart = camDist;
  }
}, {passive:true});

canvas.addEventListener('touchmove', function(e){
  if (e.touches.length === 1 && dragging){
    const dx = e.touches[0].clientX - lastX;
    const dy = e.touches[0].clientY - lastY;
    lastX = e.touches[0].clientX;
    lastY = e.touches[0].clientY;
    camYaw   -= dx * 0.008;
    camPitch += dy * 0.006;
    camPitch = Math.max(-0.15, Math.min(1.15, camPitch));
    updateCamera();
  } else if (e.touches.length === 2){
    const d = Math.hypot(
      e.touches[0].clientX - e.touches[1].clientX,
      e.touches[0].clientY - e.touches[1].clientY
    );
    if (pinchDist0 > 0){
      camDist = Math.max(3.2, Math.min(12, pinchStart * pinchDist0 / d));
      updateCamera();
    }
  }
}, {passive:true});

canvas.addEventListener('touchend', function(){ dragging = false; });

/* 桌面端鼠标 */
canvas.addEventListener('mousedown', function(e){
  dragging = true; lastX = e.clientX; lastY = e.clientY;
});
window.addEventListener('mousemove', function(e){
  if (!dragging) return;
  const dx = e.clientX - lastX, dy = e.clientY - lastY;
  lastX = e.clientX; lastY = e.clientY;
  camYaw   -= dx * 0.008;
  camPitch += dy * 0.006;
  camPitch = Math.max(-0.15, Math.min(1.15, camPitch));
  updateCamera();
});
window.addEventListener('mouseup', function(){ dragging = false; });
canvas.addEventListener('wheel', function(e){
  e.preventDefault();
  camDist = Math.max(3.2, Math.min(12, camDist + e.deltaY * 0.004));
  updateCamera();
}, {passive:false});

window.addEventListener('resize', function(){
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

/* ================= 循环:轻微呼吸 + 尾巴摇摆 ================= */
const clock = new THREE.Clock();

function tick(){
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.getElapsedTime();

  // 身体轻微呼吸
  cat.group.position.y = Math.sin(t * 1.4) * 0.02;

  // 尾巴逐级摆动
  const bases = [-1.15, 0.70, 0.60, 0.45, 0.30];
  for (let i = 0; i < cat.tail.length; i++){
    const p = cat.tail[i];
    p.rotation.x = bases[i] + Math.sin(t * 2.4 - i * 1.2) * 0.06;
    p.rotation.z = Math.sin(t * 1.8 - i * 1.4) * 0.10;
  }

  renderer.render(scene, camera);
}

renderer.setAnimationLoop(tick);

})();
</script>
</body>
</html>

Game Source: OC 猫猫 · 3D 预览

Creator: TurboKoala83

Libraries: three

Complexity: complex (442 lines, 13.5 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: oc-3d-turbokoala83" to link back to the original. Then publish at arcadelab.ai/publish.