使用Three.js为QQ用户生成3D头像阵列

东西其实比较简单,就是输出某一范围内QQ账号的3D头像

涉及的技术主要是Three.js的基本使用

而后通过腾讯的接口异步并发请求QQ用户头像,Canavs作图生成Texture应用在球体上

需要注意的是,必须修改Chrome启动参数允许访问跨域资源才可正常打开此页面

GitHub地址:https://github.com/gstoken/qq-cube

先上效果图

 

 

然后是主要代码

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="./css/bootstrap.css" />
    <style>
        html, body {
            margin: 0px;
            padding: 0px;
            width: 100%;
            height: 100%;
        }
        .page {
            position: fixed;
            width: 100%;
            height: 100%;
            background-color: white;
        }
        .top {
            padding: 10px;
            width: 100%;
            background-color: #222;
            color: white;
        }
        .content {
            width: 100%;
            height: 100%;
        }
    </style>
    <title>QQ号顺序阵列查询</title>
</head>
<body>
    <div class="page">
        <div class="top">
            <form class="form-inline">
                <div class="form-group">
                    <label for="dstQQNum">QQ号:</label>
                    <input type="number" class="form-control" id="dstQQNum" value="1982775886" placeholder="目标QQ号" />
                </div>
                <div class="form-group">
                    <label for="querySize">查询数量:</label>
                    <input type="number" class="form-control" id="querySize" value="64" placeholder="查询个数" />
                </div>
                <div class="form-group">
                    <label for="taskNum">并发数量:</label>
                    <input type="number" class="form-control" id="taskNum" value="50" placeholder="并发数量" />
                </div>
                <div class="form-group">
                    <input id="standardQuery" type="button" class="btn btn-primary" value="查询" />
                    <input id="randomQuery" type="button" class="btn btn-primary" value="随机查查" />
                    <input id="clearQQ" type="button" class="btn btn-primary" value="清空QQ" />
                </div>
            </form>
        </div>
        <div class="content">
        </div>
    </div>
    <script src="./js/jquery-3.3.1.js"></script>
    <script src="./js/three.js"></script>
    <script src="./js/OrbitControls.js"></script>
    <script src="./js/Detector.js"></script>
    <script src="./js/stats.min.js"></script>
    <script type="x-shader/x-vertex" id="vertexShader">
        varying vec3 vWorldPosition;
        void main() {
            vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
            vWorldPosition = worldPosition.xyz;
            gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        }
    </script>
    <script type="x-shader/x-fragment" id="fragmentShader">
        uniform vec3 topColor;
        uniform vec3 bottomColor;
        uniform float offset;
        uniform float exponent;
        varying vec3 vWorldPosition;
        void main() {
            float h = normalize( vWorldPosition + offset ).y;
            gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( max( h , 0.0), exponent ), 0.0 ) ), 1.0 );
        }
    </script>

    <script>
        if (!Detector.webgl) Detector.addGetWebGLMessage();


        //球体贴图宽
        const ballTextureWidth = 256;
        //球体贴图高
        const ballTextureHeight = 128;
        //头像尺寸
        const headSize = 128;
        //摄像机,场景,渲染器
        let camera, scene, renderer;
        let stats;

        let $area = $(".content");

        let meshList = [];

        //绘制图像到canvas
        function drawToCanvas (img, canvas) {
            let ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, (ballTextureHeight - headSize) / 2, headSize, headSize);
            ctx.drawImage(img, ballTextureWidth / 2, (ballTextureHeight - headSize) / 2, headSize, headSize);          
        }

        //根据QQ以及坐标位置信息生成球体对象
        function createBallMesh (img) {
            let canvas = document.createElement("canvas");
            canvas.width = ballTextureWidth;
            canvas.height = ballTextureHeight;

            let ctx = canvas.getContext("2d");
            ctx.fillStyle = "#ffffff";
            ctx.fillRect(0, 0, ballTextureWidth, ballTextureHeight);
            drawToCanvas(img, canvas);

            let texture = new THREE.Texture(canvas);
            //新建标准网孔材质
            let ballMat = new THREE.MeshStandardMaterial( {
                color: "white",
                roughness: 0.4,
                metalness: 0.4,
                map: texture
            });
            texture.needsUpdate = true;

            let ballGeometry = new THREE.SphereGeometry(0.5, 32, 32);
            let ballMesh = new THREE.Mesh(ballGeometry, ballMat);
            ballMesh.rotation.y = Math.PI;

            texture = undefined;
            canvas = undefined;

            return ballMesh;
        }

        async function buildQQMeshList (qqNumList, concurrencyNum) {
            let rtvList = [];
            let getList = await getAllQQHead(qqNumList, concurrencyNum);
            for (let i = 0; i < getList.length; ++i) {
                let curQQInfo = getList[i];
                let curQQNum = curQQInfo.qqNum;
                let curMesh = createBallMesh(curQQInfo.headImg);
                rtvList[i] = {
                    qqNum: curQQNum,
                    qqMesh: curMesh
                };
            }
            return rtvList;
        }
        
        async function draw (scene, list, concurrencyNum) {
            clear();
            meshList = await buildQQMeshList(list, concurrencyNum);
            meshList.sort((a, b) => {
                return a.qqNum - b.qqNum;
            });
            len = meshList.length;
            let size = Math.ceil(Math.cbrt(len));
            let i = 0;
            for (let z = 0; z > -size; --z) {
                for (let y = size - 1; y >= 0; --y) {
                    for (let x = 0; x < size; ++x) {
                        if (i < len) {
                            let ballMesh = meshList[i++].qqMesh;
                            ballMesh.position.set(x * 2, y * 2, z * 2);
                            scene.add(ballMesh);
                        }
                        else {
                            return;
                        }
                    }
                }
            }
        }

        function clear () {
            for (let i = 0; i < meshList.length; ++i) {
                scene.remove(meshList[i].qqMesh);
            }
        }

        async function init(element) {
            let areaWidth = $area.width();
            let areaHeight = $area.height();

            //初始化相机
            camera = new THREE.PerspectiveCamera(30, areaWidth / areaHeight, 1, 5000);
            camera.position.set(0, 0, 25);

            //初始化场景
            scene = new THREE.Scene();
            scene.background = new THREE.Color().setHSL(0.6, 0, 1);
            scene.fog = new THREE.Fog(scene.background, 1, 5000);
            
            //添加半球光源
            let = hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);
            hemiLight.color.setHSL(0.6, 1, 0.6);
            hemiLight.groundColor.setHSL(0.095, 1, 0.75);
            hemiLight.position.set(0, 50, 0);
            scene.add(hemiLight);
            //添加直接光源
            let dirLight = new THREE.DirectionalLight(0xffffff, 1);
            dirLight.color.setHSL(0.1, 1, 0.95);
            dirLight.position.set(-1, 1.75, 1);
            dirLight.position.multiplyScalar(30);
            scene.add(dirLight);

            //环境
            let groundGeo = new THREE.PlaneBufferGeometry(10000, 10000);
            let groundMat = new THREE.MeshPhongMaterial({color: 0xffffff, specular: 0x050505});
            groundMat.color.setHSL(0.095, 1, 0.75);
            let ground = new THREE.Mesh(groundGeo, groundMat);
            ground.rotation.x = -Math.PI / 2;
            ground.position.y = -33;
            scene.add(ground);
            ground.receiveShadow = true;
            
            //天幕
            var vertexShader = document.getElementById('vertexShader').textContent;
            var fragmentShader = document.getElementById('fragmentShader').textContent;
            var uniforms = {
                topColor: {value: new THREE.Color(0x0077ff)},
                bottomColor: {value: new THREE.Color(0xffffff)},
                offset: {value: 33},
                exponent: {value: 0.6}
            };
            uniforms.topColor.value.copy(hemiLight.color);
            scene.fog.color.copy(uniforms.bottomColor.value);
            var skyGeo = new THREE.SphereGeometry(4000, 32, 15);
            var skyMat = new THREE.ShaderMaterial( { vertexShader: vertexShader, fragmentShader: fragmentShader, uniforms: uniforms, side: THREE.BackSide } );
            var sky = new THREE.Mesh( skyGeo, skyMat );
            scene.add(sky);
            
            //渲染器
            renderer = new THREE.WebGLRenderer({antialias: true});
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(areaWidth, areaHeight);
            element.appendChild(renderer.domElement);
            renderer.gammaInput = true;
            renderer.gammaOutput = true;
            renderer.shadowMap.enabled = true;

            //初始化轨道控制器
            controls = new THREE.OrbitControls(camera, renderer.domElement);

            // stats = new Stats();
            // element.appendChild(stats.dom);

            window.addEventListener('resize', onWindowResize, false);
        }

        function onWindowResize() {
            let width = $area.width();
            let height = $area.height();
            camera.aspect = width / height;
            camera.updateProjectionMatrix();
            renderer.setSize(width, height);
        }

        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
            //stats.update();
        }

        (async () => {
            let element = $area[0];
            await init(element);
            animate();
        })();
    </script>

    <script>
        //根据起始QQ,结束QQ生成QQ列表
        function buildInputQQNumList (opQQNum, edQQNum) {
            let len = edQQNum - opQQNum + 1;
            let list = Array(len).fill(0).map((item, index) => {
                return opQQNum + index;
            });
            return list;
        }
        //闭区间随机选数
        function randomRange (op, ed) {
            let len = ed - op + 1;
            let offset = Math.floor(Math.random() * len);
            return op + offset;
        }
        //随机生成模拟数据
        function buildTestList (len) {
            let randOpQQNum = randomRange(10000, 100000000);
            let edQQNum = randOpQQNum + len - 1;
            return buildInputQQNumList(randOpQQNum, edQQNum);
        }
        //对列表按索引分组,传入列表和分组个数
        function batch (list, num) {
            let batchList = [];
            for (let i = 0; i < num; ++i) {
                batchList[i] = list.filter((item, index) => {
                    if (index % num == i) {
                        return true;
                    }
                    else {
                        return false;
                    }
                });
            }
            return batchList;
        } 
    </script>

    <script>
        //根据url获取Image对象,返回一个Promise
        function getImage (url, timeout) {
            timeout = timeout || 60000;
            return new Promise((resolve, reject) => {
                try {
                    let imgObj = new Image();
                    imgObj.src = url;
                    imgObj.crossOrigin = "";

                    let id = setTimeout(() => {
                        reject();
                    }, timeout);
                    
                    imgObj.onload = () => {
                        clearTimeout(id);
                        resolve(imgObj);
                    };
                    
                    imgObj.onerror = () => {
                        reject();
                    };
                }
                catch (e) {
                    reject();
                }
            });   
        }
        //获取访问QQ头像的接口url
        function getQQHeadUrl (qqNum) {
            //return "https://qlogo2.store.qq.com/qzone/" + qqNum + "/" + qqNum + "/100";
            return "http://q2.qlogo.cn/headimg_dl?dst_uin=" + qqNum + "&spec=100";
        }
        //获取QQ头像
        function getQQHead (qqNum) {
            return getImage(getQQHeadUrl(qqNum));
        }
        //根据传入的QQ账号列表生成一个Promise异步任务获取对应QQ头像
        function startGetQQHeadTask (qqNumList) {
            return new Promise(async (resolve, reject) => {
                let taskArray = [];
                for (let i = 0; i < qqNumList.length; ++i) {
                    let curQQNum = qqNumList[i];
                    let curQQHeadImg;
                    try {
                        curQQHeadImg = await getQQHead(curQQNum);
                    }
                    catch (e) {
                        curQQHeadImg = null;
                    }
                    taskArray[i] = {
                        qqNum: curQQNum,
                        headImg: curQQHeadImg
                    };
                }
                resolve(taskArray);
            });
        }
        //根据QQ账号列表获取所有头像
        function getAllQQHead (qqNumList, concurrencyNum) {
            return new Promise((resolve, reject) => {
                let rtvList = [];
                let batchList = batch(qqNumList, concurrencyNum);
                let count = 0;
                for (let i = 0; i < batchList.length; ++i) {
                    let dstTaskList = batchList[i];
                    startGetQQHeadTask(dstTaskList).then((list) => {
                        rtvList = rtvList.concat(list);
                        count++;
                        if (count >= concurrencyNum) {
                            resolve(rtvList);
                        }
                    });
                }   
            });  
        }
    </script>

    <script>
        $(function () {
            $("#standardQuery").click(async () => {
                let dstQQNum = Number(document.getElementById("dstQQNum").value);
                let querySize = Number(document.getElementById("querySize").value);
                if (querySize < 1) {
                    alert("查询个数输入错误");
                    return;
                }
                let taskNum = Number(document.getElementById("taskNum").value);
                if (taskNum < 1) {
                    alert("并发数量输入错误");
                    return;
                }
                let list = buildInputQQNumList(dstQQNum, dstQQNum + querySize - 1);
                await draw(scene, list, taskNum);
            });
            $("#randomQuery").click(async () => {
                let taskNum = Number(document.getElementById("taskNum").value);
                let list = buildTestList(4 * 4 * 4);
                await draw(scene, list, taskNum);   
            });
            $("#clearQQ").click(() => {
                clear();
            });
        });
    </script>
</body>
</html>

 

posted @ 2018-03-09 14:09  鸡毛巾  阅读(1019)  评论(0编辑  收藏  举报