一直彩红猫中毒,在学了一些three.js之后,更想做一个属于自己的彩虹猫了。

最后的完成效果:彩红猫 手机显示效果也还可以。

python彩虹猫代码 彩虹猫的技巧_Points

目标

在做一件事情之前 最为重要的就是列出要做到的事情目标,同时目标要记得是可测量的(得知道怎么样算是完成)

  • 彩红可调整猫使用帧动画进行动画效果
  • 彩红猫的速度是可以动态调整的(即帧动画速度可调整)
  • 彩红猫的贴图可以切换 切换不同风格的彩红猫
  • 循环向左的彩色背景,速度可调整
  • 切换彩红猫的时候同时更换速度与背景音乐
  • (附加)音乐可视化效果 音频音强变化展示在视觉效果上

实现过程

帧动画

实现彩红猫的帧动画其实就和在canvas 2d上绘制差不多,相比起2d的绘制,无非是将canvas贴图放在three.js 中的Sprite上面,并且更新材质信息

cat对象
function Cat(name,speed,imgPack,canvas){
    this.name =  name || 'v';
    this.speed = speed || 0.5;
    this.startTime = new Date() - 0;
    this.imgPack = imgPack || [] ;
    this.ctx = document.getElementById('cat').getContext('2d') || canvas.getContext('2d');
    this.canvas = document.getElementById('cat');
}

Cat.prototype = {
    //展示第几帧
    _drawImg(image){
        if(!image){
            return false;
        }
        var imgWidth = image.width;
        var imgHeight = image.height;
        var positionX = this.canvas.width/2 - imgWidth/2;
        var positionY = this.canvas.height/2 - imgHeight/2;
        this.ctx.drawImage(image,positionX,positionY)
    },
    animate(delta){
        this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)
        //delta时间在在运行良好的时候接近13.3ms  真实时间片
        var allTime = this.speed * this.imgPack.length * 1000;//所有帧运行一个循环所需的时间 ms 这里正好1000
        var duration = (new Date()-0) - this.startTime;//距离对象创建的时间
        var singleLoopTime = duration % allTime;//在当前循环下的时间
        var imgClipNum  = Math.floor(singleLoopTime / (this.speed * 1000));
        var showTime = this.speed * 1000;//每一帧要展示的时间
        var imgPack = this.imgPack;
        var imgToShow = imgPack[imgClipNum];
        this._drawImg(imgToShow);
        if(this.rainbow){//这里是让彩虹按照三角函数方式以时间作为参数进行动画的方法
            var rainbows = this.rainbow.children;
            var xDistance = rainbows[0].position.x - rainbows[rainbows.length-1].position.x;
            for(var x = 0 ; x < rainbows.length; x++){
                var SingleRainbow = rainbows[x];
                SingleRainbow.position.y =Math.sin((SingleRainbow.position.x + singleLoopTime/1000*(Math.PI*2))*2*(1/(this.speed*this.imgPack.length)))/5;
            }
        }
    },
}

cat对象有自己的速度,开始时间用于计算目前的时间应该绘制哪帧的图片,imgPack用于存储各个帧的图片对象,canvas元素用于暂存图像信息, 在页面中可以是隐形的。因为最终需要绘制在3d场景当中。
animate函数放置在动画主循环中,决定这个canvas在当前的时间应该绘制哪帧。

彩虹

本来想把彩虹单独作为一个对象绘制,但是想了一下彩虹也是属于彩红猫的一部分,所以还是添加到了彩虹猫对象当中

cat.prototype = {
	...
	//生成一条彩虹的方法
	initRainbow(){
        var points;
        var positions = [];
        var colors = [1.0,0.0,0.0,
            1.0,0.5,0.0,
            1.0,1.0,0.0,
            0.0,1.0,0.0,
            0.0,1.0,1.0,
            0.0,0.0,1.0];
        for(var i = 0 ; i < 6 ; i++){
            var x = 0;
            var y = (3-(i+1)+0.5)/(2);
            var z = 0;
            positions.push(x,y,z);
        }
        var PointsGeometry = new THREE.BufferGeometry();
        PointsGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
        PointsGeometry.addAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ));
        var material = new THREE.PointsMaterial( { size: 0.52, vertexColors: THREE.VertexColors } );
        points = new THREE.Points( PointsGeometry, material );
        return points;
    },
    //一条彩虹
    initAllRainBow(num){
        var p = this.initRainbow();
        var rainBowGroup = new THREE.Group();
        for(var i = 0 ; i < num ; i++){
            var x = ((num/2)-(i+1)+0.5)/2;
            var y = Math.sin(x)*0.2;
            var SingleRainbow = p.clone();
            SingleRainbow.position.set(x,y,0);
            rainBowGroup.add(SingleRainbow);
        }
        rainBowGroup.name = 'rainbow';
        rainBowGroup.position.x = -30.25;
        return rainBowGroup;
    }
}

同时将生成生成的彩虹添加到猫的属性里:

function Cat(name,speed,imgPack,canvas){
    this.rainbow = this.initAllRainBow(120);//这个彩虹一共120竖条
}

随机彩色点背景

生成彩色点比较简单。使用的是three里面的Points,这个方法封装了gl原生的点所以性能上没什么问题

function Background(name,speed){
    this.name = name || 'background';
    this.speed = speed || 0.5;
    this.startTime = new Date();
    this.points = this._initBackground();
}
Background.prototype = {
    //生成一个简单的背景1k随机点 x(-50~50) y(-50~50) z(-50~6) 
    _initBackground:function(){
        var colors = [];
        var positions = [];

        for(var i = 0 ; i < 1000 ; i++){
            //生成随机位置
            var x = Math.random()*100 - 50;
            var y = Math.random()*100 - 50;
            var z = -Math.random()*56 + 6;
            positions.push(x,y,z);
            //随机颜色
            var r = Math.random();
            var g = Math.random();
            var b = Math.random();
            colors.push(r,g,b);
        }
        var PointsGeometry = new THREE.BufferGeometry();
        PointsGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
        PointsGeometry.addAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ));
        var material = new THREE.PointsMaterial( { size: .51, vertexColors: THREE.VertexColors } );
        var points = new THREE.Points( PointsGeometry, material );
        var PointsGroup = new THREE.Group();
        for(var z = 0 ; z < 20; z ++){
            var clonedPoints = points.clone();
            clonedPoints.position.x = (10-z)*50;
            PointsGroup.add(clonedPoints);
        }
        return PointsGroup;
    }
}

此处z的范围在-50到6之间的原因是,彩虹猫以及彩虹的位置在z相机的位置距离彩红猫的位置为10,再比10大相机也看不到了,所以只要少部分点在比彩红猫离相机更近的位置,这样有种点在相机面前飘过的感觉。

创建场景 加载图片

;(function(undefined){
    'use strict';
    var _global;
    var renderer,scene,light,camera,manager,imgLoader,musicShape;
    var imgArr = [];
    var show = {
        renderer:renderer,
        scene:scene,
        light:light,
        camera:camera,
        manager:manager,
        imgLoader:imgLoader,
        imgArr:imgArr,
        musicShape:musicShape,
        cat:new THREE.Sprite(),
        init:function(){
            var canvas = document.getElementById('stage');
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
            //初始化three的加载器
            manager = initLoadingManager();
            imgLoader = initImgLoader(manager);
            this.scene = initScene();
            this.renderer = initRenderer();
            this.camera = initCamera();
            this.light = initLight();

            //引用加载器
            this.manager = manager;
            this.imgLoader = imgLoader;

            this.musicShape = initObjs();
            initTest();
            //this.camera.lookAt(scene.position);

            //var axes = new THREE.AxesHelper(20);

            var catSprite = initCat();
            catSprite.scale.set(11,11,1);
            this.cat = catSprite;
            catSprite.position.set(1,0,0);
            scene.add(catSprite);
            //scene.add(axes);
            //addControler();
        },
        render:function(){
            renderer.render(scene,camera)
        },
        //获取单个图片并且返回图片对象
        getImg(url,target,index){
            var that = this;
            that.imgLoader.load(
                url,
                function(image){
                    //加载完成之后将图片添加进去
                    if(typeof(index) === 'undefined'){
                        console.log('此处的加载需要提供索引');
                        return false;
                    }
                    target[index] = image;
                },
                undefined,
                function(){
                    console.log('加载'+url+'失败,原因是:我也不知道。。。');
                }
            )
        },
        LoadAllImgs(srcArr,tempSpace){
            var imgArr = [];
            for(var i = 0 ; i < srcArr.length;i++){
                var imgSrc = srcArr[i];
                //var img = this.getImg(imgSrc);
                this.getImg(imgSrc,tempSpace,i)
            }
        }
    };
	//这是单纯测试用的而已
    function initTest(){
        //scene.add(points2);
        var cubeG = new THREE.BoxGeometry(1,1,1);
        var CubeMaterial = new THREE.MeshPhongMaterial({
            color:0xff0000,
            transparent:true,
            opacity:0.5
        })
        var cube = new THREE.Mesh(cubeG,CubeMaterial);
        cube.position.z = -0.5;
        //scene.add(cube);

    }
    //three的加载器 加载开始加载过程中以及加载之后的事件
    function initLoadingManager(){
        var manager = new THREE.LoadingManager();
        manager.onStart = function(url, itemsLoaded, itemsTotal){
            console.log('开始加载文件:'+url+',在'+itemsTotal+'中已经加载完成的文件:'+itemsLoaded);
        };
        manager.onLoad = function(){
            console.log('所有文件加载完毕')
        };
        manager.onProgress = function(url,itemsLoaded,itemsTotal){
            console.log('正在加载文件:'+url+',已完成:'+Math.round(itemsLoaded*100/itemsTotal)+'%')
        };
        manager.onError = function(url){
            console.log('加载过程中出现失败,加载失败的文件是'+url);
        };
        return manager;
    }
    function initImgLoader(manager){
        var loader = new THREE.ImageLoader(manager);
        return loader;
    }
    //创建并且返回场景
    function initScene(){
        scene = new THREE.Scene();
        return scene;
    }
    //创建渲染器
    function initRenderer(){
        renderer = new THREE.WebGLRenderer({antialias:true,canvas:document.getElementById('stage')});
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth,window.innerHeight);
        renderer.setClearColor(0xffffff);
        return renderer;
    }
    //创建相机
    function initCamera(){
        camera = new THREE.PerspectiveCamera(90,window.innerWidth/window.innerHeight,0.1,200);
        camera.position.set(2,0,9);
        if(!IsPC()){
            camera.position.x = 6;
            camera.position.z = 9;
            console.log('手机')
        }
        camera.lookAt(scene.position);
        return camera;
    }
    //在此处其实没什么意义 创建灯光的, 而在彩红猫这个项目中没有使用会因为灯光而影响的材质 可以去掉
    function initLight(){
        scene.add(new THREE.AmbientLight(0x444444));
        light = new THREE.DirectionalLight(0xffffff);
        light.position.set(-20, 30, 30);

        light.shadow.camera.top = 10;
        light.shadow.camera.bottom = -10;
        light.shadow.camera.left = -10;
        light.shadow.camera.right = 10;

        light.castShadow = true;

        scene.add(light);
        return light;
    }
    //最开始的音频可视化用了长条形方块 就和另外一个博客中一样
    function initObjs(){//此函数生成一个会根据声音来改变形状或者位置的形状组
        var group = new THREE.Group();
        for(var i = 0 ; i < 128 ; i++){
            var cubeG = new THREE.BoxGeometry(0.1,0.1,0.1);
            var cubeM = new THREE.MeshPhongMaterial({color:0xff00ff})
            var cube = new THREE.Mesh(cubeG,cubeM);
            var x = (64-i)*(20/128) - 5;
            var y = -3;
            var z = 2;
            cube.position.set(x,y,z);
            group.add(cube);
        }
        var transparentCubeG = new THREE.BoxGeometry(20,10,0.2);
        var transparentCubeM = new THREE.MeshBasicMaterial({
            color:0xffffff,
            transparent:true,
            opacity:1,
        })
        var transparentCube = new THREE.Mesh(transparentCubeG,transparentCubeM);
        transparentCube.position.set(0,-8,2);
        //group.add(transparentCube);
        //scene.add(group);
        return group;
    }
    function initCat(canvas){
        canvas = canvas || document.getElementById('cat');
        if(canvas){
            var spriteMaterial = new THREE.SpriteMaterial({
                map:new THREE.CanvasTexture(canvas),
            });
            return new THREE.Sprite(spriteMaterial);
        }else{
            console.log('nothing will init')
        }
    }
    _global = (function(){return this || (0,eval)('this')}());
    if(typeof module !== 'undefined' && module.exports){
        module.exports = show;
    }else if(typeof define === 'function' && define.amd){
        define(function(){return show});
    }else{
        !('plugin' in _global) && (_global.show = show);
    }
}());

开始初始化的过程

show.init();
var cats = {
    v:getCatsImgs('nyan'),
    original:getCatsImgs('original'),
    technyan:getCatsImgs('technyancolor')
}

function getCatsImgs(name){
    var technyancolor00;
    var arr = [];
    for(var x = 0 ;x < 12 ; x++){
        var str = 'imgs/nyan/'+name+'0'+x+'.png';
        if(x>=10){str = 'imgs/nyan/'+name+x+'.png'};
        arr.push(str);
    }
    return arr;
}

var delta = new Date() - 0;
var cat = new Cat('v2',1/12);

show.LoadAllImgs(cats.v,
    cat.imgPack);

var bg = new Background('BG',1);
console.log(bg.points);

var mv = new Musicvisualizer({
    size:128,
    draw:function(){
    }
})
//这个很可能会被策略拦截掉,按按钮才能播放声音
mv.play('https://towrabbit.oss-cn-beijing.aliyuncs.com/three/media/vday.ogg');

function loop(){
    var now = new Date() - 0;
    var d = (now - delta)/(1000/60);
    delta = now;
    update(d);//数据层更新
    render();
    //将渲染层的更新放在这
    requestAnimationFrame(loop);
}
function render(){
    show.renderer.render(show.scene,show.camera);
    //requestAnimationFrame(render);
}
function update(deltaTime){
    var arr = mv.getFrequencyArr();
    var max = 1;
    for(var i = 15 ; i < show.musicShape.children.length; i++){
        var currentValue = arr[i];
        if(currentValue > max){max = currentValue};
        //获取某一端频率声音内最响的音频 让彩红猫和彩虹的大小随之变化
    }
    var p = max/256;
    cat.rainbow.scale.y = p*0.5+1;
    show.cat.scale.x = p*5+11;
    show.cat.scale.y = p*5+11;
    bg.points.position.x -= 0.16*deltaTime*bg.speed;
    if(bg.points.position.x<=-400){
        bg.points.position.x +=700;
    }
    cat.animate();
    show.cat.material.map.needsUpdate = true;
    //此处deltaTime真实事件片段 在60帧的时候接近13.3ms
}
window.onload = function(){
    addControler();
    console.log(cat.imgPack);
    //cat._drawImg(cat.imgPack[0]);
    document.getElementById('stage').addEventListener('mousedown',function(){
        console.log('yes?')
        //playmusic();
    });
    show.scene.add(bg.points);
    show.scene.add(cat.rainbow);
    loop();
};
function addControler(){
    var controls;
    controls = new THREE.OrbitControls( show.camera, show.renderer.domElement );
    controls.enableDamping = true;
    controls.dampingFactor = 0.25;
    controls.screenSpacePanning = false;
    controls.minDistance = 5;
    controls.maxDistance = 30;
    controls.maxPolarAngle = Math.PI / 2;
}
function IsPC(){
    var userAgentInfo = navigator.userAgent;
    var Agents = new Array("Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod");
    var flag = true;
    for (var v = 0; v < Agents.length; v++) {
        if (userAgentInfo.indexOf(Agents[v]) > 0) { flag = false; break; }
    }
    return flag;
}
function playmusic(){
    var media = document.getElementById('music');
    console.log('mousedown'+media.paused);
    if(media && media.paused){
        media.play();
        console.log('play');
    }
}
var catType = 0;
function changeCat(){
    catType = catType+=1;
    if(catType >= 3){
        catType = 0;
    }
    var catname = '';
    switch(catType){
        case 0:
            catname = 'v';
            show.cat.scale.set(11,11,1);
            mv.play('http://towrabbit.oss-cn-beijing.aliyuncs.com/three/media/vday.ogg');
            cat.speed = 1/12;
            bg.speed = 1;
            break;
        case 1:
            catname = 'original';
            show.cat.scale.set(11,11,1);
            mv.play('http://towrabbit.oss-cn-beijing.aliyuncs.com/three/media/original.ogg')
            cat.speed = 1/14;
            bg.speed = 14/12;
            break;
        case 2:
            catname = 'technyan';
            show.cat.scale.set(12,12,1);
            mv.play('http://towrabbit.oss-cn-beijing.aliyuncs.com/three/media/technyancolor.mp3')
            cat.speed = 1/18;
            bg.speed = 18/12;
            break;
    }
    show.LoadAllImgs(cats[catname],cat.imgPack);
}

后记

额感觉有好多东西没说

其中的音频可视化在另外一个文件中,不详细阐述
所有的文件源码可以在网站源文件中查看到
直接f12就行-0-
或者github彩红猫-towrabbit

towrabbit

欢迎大家评论点赞