OC 猫猫 · 3D 预览(精修版)
by TurboKoala83466 lines14.3 KB🛠️ Three.js (3D graphics)
<!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,.4);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(40, innerWidth/innerHeight, 0.1, 100);
/* ================= 灯光 ================= */
scene.add(new THREE.HemisphereLight(0xdfeaff, 0x2a2a30, 1.0));
const key = new THREE.DirectionalLight(0xfff0d8, 1.1);
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.4);
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, 32, 24), matMain);
body.scale.set(0.92, 0.82, 1.55); // X收窄, Y压低, Z拉长
body.position.set(0, 0.85, -0.02);
body.castShadow = true;
g.add(body);
// 臀部(后段)
const hip = new THREE.Mesh(new THREE.SphereGeometry(0.48, 32, 24), matMain);
hip.scale.set(0.95, 0.88, 1.1);
hip.position.set(0, 0.88, -0.55);
hip.castShadow = true;
g.add(hip);
// 颈部过渡(连接头和身体)
const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.38, 0.4, 16), matMain);
neck.position.set(0, 1.12, 0.42);
neck.rotation.x = -0.3;
neck.castShadow = true;
g.add(neck);
// 前胸浅色
const chest = new THREE.Mesh(new THREE.SphereGeometry(0.4, 32, 24), matLight);
chest.scale.set(0.92, 0.85, 0.9);
chest.position.set(0, 0.67, 0.48);
g.add(chest);
/* ============ 头部 ============ */
const head = new THREE.Mesh(new THREE.SphereGeometry(0.42, 32, 24), matMain);
head.scale.set(1.15, 0.98, 1.0);
head.position.set(0, 1.32, 0.78);
head.castShadow = true;
g.add(head);
// 下巴(略微前突,让脸型更尖)
const chin = new THREE.Mesh(new THREE.SphereGeometry(0.3, 20, 16), matMain);
chin.scale.set(1.0, 0.7, 1.0);
chin.position.set(0, 1.18, 0.88);
g.add(chin);
// 脸颊毛簇(3根斜向外)
[-1, 1].forEach(function(sx){
for (let i = 0; i < 3; i++){
const spike = new THREE.Mesh(new THREE.ConeGeometry(0.065, 0.26, 8), matMain);
spike.rotation.z = -sx * (Math.PI/2 + 0.2);
spike.rotation.y = 0.1 * sx;
spike.position.set(0.44*sx, 1.34 - i*0.09, 0.78);
g.add(spike);
}
});
// 口鼻
const muzzle = new THREE.Mesh(new THREE.SphereGeometry(0.22, 24, 20), matLight);
muzzle.scale.set(1.18, 0.85, 1.0);
muzzle.position.set(0, 1.16, 1.04);
g.add(muzzle);
// 鼻子(倒三角感)
const nose = new THREE.Mesh(new THREE.ConeGeometry(0.065, 0.08, 4), matLine);
nose.rotation.x = Math.PI;
nose.rotation.y = Math.PI/4;
nose.position.set(0, 1.22, 1.22);
g.add(nose);
// 嘴线
const mouthGeo = new THREE.TorusGeometry(0.07, 0.01, 6, 12, 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.11, 1.18);
g.add(m);
});
/* ============ 眼睛(倒三角凶狠眼型,加大) ============ */
[-1, 1].forEach(function(sx){
// 眼白
const eyeW = new THREE.Mesh(new THREE.SphereGeometry(0.115, 24, 18), matEyeW);
eyeW.scale.set(1.0, 0.78, 0.45);
eyeW.position.set(0.20*sx, 1.42, 1.06);
eyeW.rotation.z = 0.3 * sx;
g.add(eyeW);
// 绿色虹膜
const eyeG = new THREE.Mesh(new THREE.SphereGeometry(0.092, 24, 18), matEyeG);
eyeG.scale.set(0.95, 0.85, 0.45);
eyeG.position.set(0.20*sx, 1.42, 1.09);
eyeG.rotation.z = 0.3 * sx;
g.add(eyeG);
// 竖瞳
const pupil = new THREE.Mesh(new THREE.SphereGeometry(0.045, 16, 12), matEyeB);
pupil.scale.set(0.45, 1.0, 0.5);
pupil.position.set(0.20*sx, 1.42, 1.13);
g.add(pupil);
// 高光
const spark = new THREE.Mesh(new THREE.SphereGeometry(0.022, 8, 6), matEyeW);
spark.position.set(0.165*sx, 1.46, 1.15);
g.add(spark);
});
/* ============ 耳朵 ============ */
[-1, 1].forEach(function(sx){
const ear = new THREE.Mesh(new THREE.ConeGeometry(0.19, 0.52, 12), matMain);
ear.position.set(0.28*sx, 1.8, 0.7);
ear.rotation.z = -0.22 * sx;
ear.rotation.x = -0.15;
ear.castShadow = true;
g.add(ear);
const inner = new THREE.Mesh(new THREE.ConeGeometry(0.105, 0.3, 12), matDark);
inner.position.set(0.28*sx, 1.78, 0.74);
inner.rotation.z = -0.22 * sx;
inner.rotation.x = -0.15;
g.add(inner);
// 耳尖簇毛
const tuft = new THREE.Mesh(new THREE.ConeGeometry(0.055, 0.18, 8), matDark);
tuft.position.set(0.28*sx, 2.08, 0.7);
tuft.rotation.z = -0.26 * sx;
tuft.rotation.x = -0.15;
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.015, 0.015, 0.2, 6),
matDark
);
s.rotation.x = Math.PI/2;
s.rotation.z = -0.4 * sx;
s.position.set(0.12*sx, 1.65 - i*0.03, 0.9 - i*0.08);
g.add(s);
}
});
/* ============ 四肢(增加关节感) ============ */
const legGeo = new THREE.CylinderGeometry(0.11, 0.09, 0.7, 16);
legGeo.translate(0, -0.35, 0);
const pawGeo = new THREE.SphereGeometry(0.12, 16, 12);
const legs = [];
const legPos = [
[ 0.32, 0.5], // 左前
[-0.32, 0.5], // 右前
[ 0.34, -0.52], // 左后
[-0.34, -0.52] // 右后
];
legPos.forEach(function(p, index){
const leg = new THREE.Mesh(legGeo, matMain);
leg.position.set(p[0], 0.75, p[1]);
leg.castShadow = true;
// 前腿微向前伸,后腿微向后蹬
if (index < 2) leg.rotation.x = 0.15;
else leg.rotation.x = -0.15;
// 腿环纹
const stripe = new THREE.Mesh(
new THREE.CylinderGeometry(0.113, 0.113, 0.06, 16),
matDark
);
stripe.position.y = -0.18;
leg.add(stripe);
// 爪子
const paw = new THREE.Mesh(pawGeo, matLight);
paw.scale.set(1.0, 0.7, 1.35);
paw.position.set(0, -0.7, 0.04);
paw.rotation.x = 0.15;
leg.add(paw);
// 脚趾
for (let t = -1; t <= 1; t++){
const toe = new THREE.Mesh(new THREE.SphereGeometry(0.035, 8, 6), matLight);
toe.position.set(t * 0.055, -0.73, 0.15);
toe.rotation.x = 0.15;
leg.add(toe);
}
g.add(leg);
legs.push(leg);
});
/* ============ 尾巴(圆柱分段,向后上方弯曲,精细过渡) ============ */
const tailSegs = [
{ len: 0.24, r0: 0.135, r1: 0.115, ang: -1.1 },
{ len: 0.30, r0: 0.115, r1: 0.098, ang: 0.7 },
{ len: 0.34, r0: 0.098, r1: 0.082, ang: 0.55 },
{ len: 0.34, r0: 0.082, r1: 0.068, ang: 0.4 },
{ len: 0.28, r0: 0.068, r1: 0.055, ang: 0.3 }
];
const tailRoot = new THREE.Group();
tailRoot.position.set(0, 1.02, -0.88);
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, 16); // 圆柱分段数增加到16
geo.translate(0, s.len/2, 0);
const mesh = new THREE.Mesh(geo, isTip ? matLight : matMain);
mesh.castShadow = true;
pivot.add(mesh);
// 深色环纹
if (!isTip){
const ring = new THREE.Mesh(
new THREE.CylinderGeometry(s.r0*1.02, s.r0*1.02, 0.05, 16),
matDark
);
ring.position.y = s.len * 0.55;
pivot.add(ring);
}
// 尾尖圆头
if (isTip){
const cap = new THREE.Mesh(new THREE.SphereGeometry(s.r1, 16, 12), 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.14, 0.022, 6, 24),
matDark
);
spiral.scale.set(1, 1, 0.18);
spiral.rotation.y = Math.PI/2;
spiral.position.set(0.48*sx, 0.88, -0.05);
g.add(spiral);
});
/* ============ 背部条纹 ============ */
for (let i = 0; i < 4; i++){
const stripe = new THREE.Mesh(
new THREE.CylinderGeometry(0.022, 0.022, 0.28, 6),
matDark
);
stripe.rotation.x = Math.PI/2;
stripe.rotation.z = 0.35;
stripe.position.set(0, 1.18 - i*0.015, -0.1 - i*0.18);
g.add(stripe);
}
return { group: g, legs: legs, tail: tailPivots };
}
/* ================= 实例化 ================= */
const cat = createCat();
scene.add(cat.group);
/* ================= 拖动旋转 / 双指缩放 ================= */
let camDist = 6.5;
let camYaw = Math.PI * 0.25;
let camPitch= 0.3;
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.1, cz);
camera.lookAt(0, 1.1, 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.1, Math.min(1.1, 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.5, 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.1, Math.min(1.1, camPitch));
updateCamera();
});
window.addEventListener('mouseup', function(){ dragging = false; });
canvas.addEventListener('wheel', function(e){
e.preventDefault();
camDist = Math.max(3.5, 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.015;
// 尾巴逐级摆动
const bases = [-1.1, 0.7, 0.55, 0.4, 0.3];
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.12;
}
renderer.render(scene, camera);
}
renderer.setAnimationLoop(tick);
})();
</script>
</body>
</html>Game Source: OC 猫猫 · 3D 预览(精修版)
Creator: TurboKoala83
Libraries: three
Complexity: complex (466 lines, 14.3 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-mujfxpme" to link back to the original. Then publish at arcadelab.ai/publish.