見出し画像

「星の形」を考える。※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>

使わなくて良いです(重要)。

起動すると、

小星形十二面体

が出ます。

少しいじると、


大星形十二面体

が出ます。

少しいじると、

十二枚の星


が出ます(?)。


結局の所、どれが正解なのか?


解なし(?????)



追記


小星形十二面体から大星形十二面体まで


終わりです。




いいなと思ったら応援しよう!

Rikuriku|フォロバ100 ⚠️警告⚠️ もしあなたがチップをくれたら、その分あなたのお金が減ります(?)。お金は大切に。