「星の形」を考える。※GIF付き
おはこんばんにちは
Rikurikuです~。
皆さん、子供が描く星を想像してください。

まあ、大抵はこんな感じですよね(偏見)。
実際これが星のイメージなんだから、
こういう形をしているんでしょう(偏見)。
では、ちょっと考えてみましょう。
AIを駆使してとあるコードを作りました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dodecahedron Viewer</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #ffffff;
}
canvas {
display: block;
}
.slider-container {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
text-align: center;
font-family: sans-serif;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 8px;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div class="slider-container">
<label for="explodeSlider">Face Distance</label><br>
<input type="range" id="explodeSlider" min="-2" max="2" step="0.01" value="0" style="width: 200px;">
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// Scene Setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff); // White background
// Camera Setup
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 3;
// Renderer Setup
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040, 1.5); // Soft white light
scene.add(ambientLight);
// Headlamp: Attach DirectionalLight to camera so it stays with the viewer
const directionLight = new THREE.DirectionalLight(0xffffff, 2);
directionLight.position.set(0, 0, 1); // From camera towards scene
camera.add(directionLight);
scene.add(camera); // Required because light is child of camera
// Geometry: Star Dodecahedron Generation
// 1. Create base Dodecahedron to get face data
const baseGeometry = new THREE.DodecahedronGeometry(1).toNonIndexed();
const basePos = baseGeometry.attributes.position;
const baseNorm = baseGeometry.attributes.normal;
const newPositions = [];
const newNormals = [];
// Helper to compare vectors with tolerance
function isSame(v1, v2) {
return v1.distanceTo(v2) < 0.01; // Relaxed tolerance
}
// Group vertices by Face Normal to identify pentagons
const faces = [];
// Instead of a map key, let's iterate and group by dot product similarity
for (let i = 0; i < baseNorm.count; i += 3) {
const n = new THREE.Vector3().fromBufferAttribute(baseNorm, i);
const v1 = new THREE.Vector3().fromBufferAttribute(basePos, i);
const v2 = new THREE.Vector3().fromBufferAttribute(basePos, i + 1);
const v3 = new THREE.Vector3().fromBufferAttribute(basePos, i + 2);
// Find existing face with similar normal
let face = faces.find(f => f.normal.dot(n) > 0.999);
if (!face) {
face = { normal: n, vertices: [] };
faces.push(face);
}
face.vertices.push(v1, v2, v3);
}
// Process each face to create a Star
faces.forEach(face => {
// 1. Deduplicate vertices
const uniqueVerts = [];
face.vertices.forEach(v => {
if (!uniqueVerts.some(uv => isSame(uv, v))) {
uniqueVerts.push(v);
}
});
if (uniqueVerts.length !== 5) {
// If it's not 5, something is wrong with the tolerance or grouping.
// It might be a regular dodecahedron always has 5 verts per face.
// Just in case, let's try to handle it or skip gracefully.
console.warn("Face is not a pentagon?", uniqueVerts.length);
return;
}
// 2. Calculate Center
const center = new THREE.Vector3();
uniqueVerts.forEach(v => center.add(v));
center.divideScalar(5);
// 3. Sort vertices
// Create a basis frame ON the face plane
const normal = face.normal.clone().normalize();
// Robust base vector selection
let up = new THREE.Vector3(0, 1, 0);
if (Math.abs(normal.dot(up)) > 0.9) up.set(0, 0, 1);
const tangent = new THREE.Vector3().crossVectors(normal, up).normalize();
const bitangent = new THREE.Vector3().crossVectors(normal, tangent);
uniqueVerts.sort((a, b) => {
const vecA = new THREE.Vector3().subVectors(a, center);
const vecB = new THREE.Vector3().subVectors(b, center);
const angleA = Math.atan2(vecA.dot(bitangent), vecA.dot(tangent));
const angleB = Math.atan2(vecB.dot(bitangent), vecB.dot(tangent));
return angleA - angleB;
});
// 4. Create Star Geometry
// Need Tip Angle = 36 degrees.
// Triangle formed by Top(Tip), Left(V1), Right(V2).
// Side length of pentagon (s) = distance(V1, V2).
// Triangle altitude (h) from midpoint to Tip.
// tan(36/2) = tan(18) = (s/2) / h
// h = (s/2) / tan(18)
for (let i = 0; i < 5; i++) {
const valley = uniqueVerts[i];
const nextValley = uniqueVerts[(i + 1) % 5];
const sideLength = valley.distanceTo(nextValley);
const h = (sideLength / 2.0) / Math.tan(THREE.MathUtils.degToRad(18));
const midpoint = new THREE.Vector3().addVectors(valley, nextValley).multiplyScalar(0.5);
// Direction from Center to Midpoint is roughly the direction to extend (for regular pentagon)
// Actually, strict geometry: direction is Midpoint - Center normalized?
// Yes, for a regular pentagon, the apothem aligns with the star point direction.
const dir = new THREE.Vector3().subVectors(midpoint, center).normalize();
// Tip Position = Midpoint + dir * h
const tip = new THREE.Vector3().copy(midpoint).add(dir.multiplyScalar(h));
// Triangle 1: Center -> Valley -> Tip
newPositions.push(center.x, center.y, center.z);
newPositions.push(valley.x, valley.y, valley.z);
newPositions.push(tip.x, tip.y, tip.z);
newNormals.push(normal.x, normal.y, normal.z);
newNormals.push(normal.x, normal.y, normal.z);
newNormals.push(normal.x, normal.y, normal.z);
// Triangle 2: Center -> Tip -> NextValley
newPositions.push(center.x, center.y, center.z);
newPositions.push(tip.x, tip.y, tip.z);
newPositions.push(nextValley.x, nextValley.y, nextValley.z);
newNormals.push(normal.x, normal.y, normal.z);
newNormals.push(normal.x, normal.y, normal.z);
newNormals.push(normal.x, normal.y, normal.z);
}
});
// Build Final Geometry
let geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(newPositions, 3));
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(newNormals, 3));
// Store original positions and normals for the explode effect
const positionAttribute = geometry.attributes.position;
const normalAttribute = geometry.attributes.normal;
const originalPositions = positionAttribute.array.slice();
// Material: Bright Light Blue
const material = new THREE.MeshPhongMaterial({
color: 0x00BFFF,
flatShading: true,
shininess: 30,
polygonOffset: true,
polygonOffsetFactor: 1, // Push mesh back slightly to prevent z-fighting with edges
polygonOffsetUnits: 1,
side: THREE.DoubleSide
});
const dodecahedron = new THREE.Mesh(geometry, material);
scene.add(dodecahedron);
// Edges: Make boundaries visible
const edgesMaterial = new THREE.LineBasicMaterial({ color: 0x005580, linewidth: 2 }); // Darker blue edges
let edges = new THREE.LineSegments(new THREE.EdgesGeometry(geometry), edgesMaterial);
dodecahedron.add(edges);
// Controls: OrbitControls handles the rotation logic correctly (avoiding simple Euler gimbal lock)
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // Smooth rotation
controls.dampingFactor = 0.05;
controls.enablePan = false; // Keep object centered
// Interaction Logic:
// Left click drag rotates the view (OrbitControls default).
// This effectively changes the "viewing angle" as requested, while keeping the object stationary or appearing to rotate.
// Explode Function
function updateExplode(distance) {
const positions = geometry.attributes.position.array;
const normals = geometry.attributes.normal.array;
for (let i = 0; i < positions.length; i += 3) {
// Determine face center direction?
// Actually for non-indexed flat geometry, the vertex normal points away from center of the face usually?
// For a Dodecahedron, vertex normals of flat faces ARE the face normals. It works perfectly.
positions[i] = originalPositions[i] + normals[i] * distance;
positions[i + 1] = originalPositions[i + 1] + normals[i + 1] * distance;
positions[i + 2] = originalPositions[i + 2] + normals[i + 2] * distance;
}
geometry.attributes.position.needsUpdate = true;
// Re-generate edges to match new geometry
dodecahedron.remove(edges);
edges.geometry.dispose();
edges = new THREE.LineSegments(new THREE.EdgesGeometry(geometry), edgesMaterial);
dodecahedron.add(edges);
}
// Listener
document.getElementById('explodeSlider').addEventListener('input', (e) => {
updateExplode(parseFloat(e.target.value));
});
// Animation Loop
function animate() {
requestAnimationFrame(animate);
controls.update(); // Required if damping or autoRotate
renderer.render(scene, camera);
}
animate();
// Handle Window Resize
window.addEventListener('resize', onWindowResize, false);
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
</script>
</body>
</html>使わなくて良いです(重要)。
起動すると、

小星形十二面体
が出ます。
少しいじると、

大星形十二面体
が出ます。
少しいじると、

十二枚の星
が出ます(?)。
結局の所、どれが正解なのか?



解なし(?????)
追記
小星形十二面体から大星形十二面体まで

終わりです。
いいなと思ったら応援しよう!
⚠️警告⚠️
もしあなたがチップをくれたら、その分あなたのお金が減ります(?)。お金は大切に。