中秋节听夜曲,Android OpenGL 呈现周董专属的玉兔主题音乐播放器

news2025/1/12 12:03:39

概述

前几天发现QQ音乐有个好玩的功能,为用户提供了多种 播放器主题,其中 原神 的主题让我眼前一亮:

转存失败,建议直接上传图片文件

当然,诸如 换肤、主题 类的功能已经屡见不鲜,但这类沉浸式播放器的听歌体验确实不错。

见猎心喜,正好中秋马上就到,我也尝试整个 中秋主题音乐播放器 试试水。

整体思路有2点:

首先是技术方面,纯粹的 ImageView 图层堆砌来实现,渲染效率太低OpenGL 是一个不错的技术方案(QQ应该也是这么实现的),顺便复习下图形学的知识。

其次是玩法上,干脆在基础的功能上加一些 更好玩的,比如为播放页设计多个图层,通过陀螺仪+图层联动实现的 裸眼3D 的视觉效果,边听歌边玩。后续还可以考虑通过制定 设计规范,让不同图层的UI元素,达成更多新奇好玩的 联动效果

说了这么多,最后效果如下所示,左侧展示录屏效果,右侧是裸眼3D效果:

## 具体实现

1. 裸眼3D原理

2年前自如的 《自如客APP裸眼3D效果的实现》 一文引发了社区的热烈讨论和实践,本着不重复造轮子的原则,这里简单对原理介绍,感兴趣的读者可参考上述链接。

裸眼 3D 效果的本质是——将整个图片结构分为 3 层:上层、中层、以及底层。在手机左右上下旋转时,上层和底层的图片呈相反的方向进行移动,中层则不动,在视觉上给人一种 3D 的感觉:

本文的效果是由以下四张图,由底至顶,依次绘制而成的:

接下来,如何感应手机的旋转状态,并将4层图片进行对应的移动呢?当然是使用设备自身提供的 传感器 了,通过传感器不断回调获取设备的旋转状态,对 UI 进行对应地渲染即可。

2. 为何选择 OpenGL

GPU 更适合图形、图像的处理,裸眼3D效果中有大量的 旋转缩放位移 操作,都可在 java 层通过一个 矩阵 对几何变换进行描述,通过 shader 小程序中交给 GPU 处理 ——理论上 OpenGL 的渲染性能比原生的 ImageView 更好。

借助OpenGLAPI,渲染性能也符合预期,打开 布局边界GPU过渡绘制 选项后,播放页渲染性能也依然稳定,更不会增加布局层级的复杂度,直接证明了该方案 具备应用到实际生产项目的可行性

3 代码实现

本文重点是描述 OpenGL 绘制时的思路描述,因此下文仅展示部分核心代码,对具体实现感兴趣的读者可参考文末的链接。

3.1 绘制静态图片

首先需要将4张图片(图片素材来源)依次进行静态绘制,这里涉及大量 OpenGL API 的使用,不熟悉的读可略读本小节,以捋清思路为主。

首先看一下顶点和片元着色器的 shader 代码,其定义了图像纹理是如何在GPU中处理渲染的:

// 顶点着色器代码
// 顶点坐标
attribute vec4 av_Position;
// 纹理坐标
attribute vec2 af_Position;
uniform mat4 u_Matrix;
varying vec2 v_texPo;

void main() {
    v_texPo = af_Position;
    gl_Position =  u_Matrix * av_Position;
}
// 片元着色器代码
precision mediump float;
// 纹理坐标
varying vec2 v_texPo;
uniform sampler2D sTexture;
void main() {
    gl_FragColor=texture2D(sTexture, v_texPo);
}

定义好了 Shader ,接下来在 GLSurfaceView (可以理解为 OpenGL 中的画布) 创建时,初始化Shader小程序,并将图像纹理依次加载到GPU中:

public class ZQRenderer implements GLSurfaceView.Renderer {
  
  @Override
  public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      // 1.加载shader小程序
      mProgram = loadShaderWithResource(mContext, R.raw.projection_vertex_shader, R.raw.projection_fragment_shader);

      //...
      
      // 2. 依次将切图纹理传入GPU
      this.texImageInner(R.drawable.icon_player_bg, mBackTextureId);
      this.texImageInner(R.drawable.icon_player_moon, mMidTextureId);
      this.texImageInner(R.drawable.icon_album_cover_nocturne, mCoverTextureId);
      this.texImageInner(R.drawable.icon_player_text, mFrontTextureId);
  }
}

接下来是定义视口以及投影矩阵,因为切图的比例各不相同,为了保证视觉效果,需要针对不同层级的图片,设置不同的正交投影策略。

public class ZQRenderer implements GLSurfaceView.Renderer {
  
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        //设置大小位置
        GLES20.glViewport(0, 0, width, height);

        Matrix.setIdentityM(mBgProjectionMatrix, 0);
        Matrix.setIdentityM(mMoonProjectionMatrix, 0);
        Matrix.setIdentityM(mCoverProjectionMatrix, 0);

        // 计算宽高比
        boolean isVertical = width < height;
        float screenRatio = (float) width / (float) height;

        // 设置投影矩阵
        // 1.深色背景图的投影矩阵,只需要铺全屏

        // 2.月亮和装饰图的投影矩阵
        float ratio = (float) 1080 / (float) 1528;
        if (isVertical) {
            Matrix.orthoM(mMoonProjectionMatrix, 0, -1f, 1f, -1f / ratio, 1f / ratio, -1f, 1f);
        } else {
            Matrix.orthoM(mMoonProjectionMatrix, 0, -ratio, ratio, -1f, 1f, -1f, 1f);
        }

        // 3.歌曲封面图投影矩阵
        if (isVertical) {
            Matrix.orthoM(mCoverProjectionMatrix, 0, -1f, 1f, -1f / screenRatio, 1f / screenRatio, -1f, 1f);
        } else {
            Matrix.orthoM(mCoverProjectionMatrix, 0, -screenRatio, screenRatio, -1f, 1f, -1f, 1f);
        }
    }
}

最后就是 绘制,读者需要理解,对于4层图像的渲染,其逻辑是基本一致的,差异仅仅有2点:图像本身不同 以及 图像的几何变换不同

public class ZQRenderer implements GLSurfaceView.Renderer {
  
  @Override
     public void onDrawFrame(GL10 gl) {
         GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
         GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
 
         GLES20.glUseProgram(mProgram);
 
         this.updateMatrix();
 
         this.drawLayerInner(mBackTextureId, mTextureBuffer, mBackMatrix);  // 画背景
         this.drawLayerInner(mMidTextureId, mTextureBuffer, mMoonMatrix);   // 画月亮
         this.drawLayerInner(mCoverTextureId, mTextureBuffer, mCoverMatrix);  // 画封面
         this.drawLayerInner(mFrontTextureId, mTextureBuffer, mFrontMatrix);  // 画前景装饰
     }
 
     private void texImageInner(@DrawableRes int drawableRes, int textureId) {
         //绑定纹理
         GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
         //环绕方式
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
         //过滤方式
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
         GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
 
         GLES20.glEnable(GLES20.GL_BLEND);
         GLES20.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
 
         Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), drawableRes);
         GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
         bitmap.recycle();
     }
}

现在我们完成了图像的 静态绘制,接下来我们需要接入 传感器,并引入不同层级图片各自的几何变换, 让图片动起来

3.2 让图片动起来

首先我们需要对 Android 平台上的传感器进行注册,监听手机的旋转状态,并拿到手机 xy 轴的旋转角度。

// 2.1 注册传感器
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
mAcceleSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mMagneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mSensorManager.registerListener(mSensorEventListener, mAcceleSensor, SensorManager.SENSOR_DELAY_GAME);
mSensorManager.registerListener(mSensorEventListener, mMagneticSensor, SensorManager.SENSOR_DELAY_GAME);

// 2.2 不断接受旋转状态
private final SensorEventListener mSensorEventListener = new SensorEventListener() {
    @Override
    public void onSensorChanged(SensorEvent event) {
        // ... 省略具体代码
        float[] values = new float[3];
        float[] R = new float[9];
        SensorManager.getRotationMatrix(R, null, mAcceleValues, mMageneticValues);
        SensorManager.getOrientation(R, values);
        // x轴的偏转角度
        float degreeX = (float) Math.toDegrees(values[1]);
        // y轴的偏转角度
        float degreeY = (float) Math.toDegrees(values[2]);
        // z轴的偏转角度
        float degreeZ = (float) Math.toDegrees(values[0]);
        
        // 拿到 xy 轴的旋转角度,进行矩阵变换
        updateMatrix(degreeX, degreeY);
    }
};

注意,因为我们只需控制图像的左右和上下移动,因此,我们只需关注设备本身 x 轴和 y 轴的偏转角度。但如果将图片直接进行位移操作,将会因为位移后图像的另一侧没有纹理数据,导致渲染结果有黑边现象,为了避免这个问题,我们需要将图像默认从中心点进行放大,保证图像移动的过程中,不会超出自身的边界。

也就是说,我们一开始进入时,看到的肯定只是图片的部分区域。给每一个图层设置 scale,将图片进行放大。显示窗口是固定的,那么一开始只能看到图片的正中位置。(中层可以不用,因为中层本身是不移动的,所以也不必放大)

这里的处理参考自 Nayuta 的 这篇文章,内部已经将思路阐述的非常清晰,强烈建议读者进行阅读。

明白了这一点,我们就能理解,裸眼 3D 的效果实际上就是对 不同层级的图像 进行 缩放位移 的变换,下面是分别获取几何变换的代码:

public class ZQRenderer implements GLSurfaceView.Renderer {
  
  private float[] mBgProjectionMatrix = new float[16];
  private float[] mMoonProjectionMatrix = new float[16];
  private float[] mCoverProjectionMatrix = new float[16];

  private float[] mBackMatrix = new float[16];
  private float[] mMoonMatrix = new float[16];
  private float[] mCoverMatrix = new float[16];
  private float[] mFrontMatrix = new float[16];

  // 封面图旋转一圈的时间,单位秒.
  private static final long ROTATE_TIME = 20L;
  public static final long DELAY_INTERVAL = 1000 / (360 / ROTATE_TIME);

  /**
   * 陀螺仪数据回调,更新各个层级的变换矩阵.
   *
   * @param degreeX x轴旋转角度,图片应该上下移动
   * @param degreeY y轴旋转角度,图片应该左右移动
   */
   private void updateMatrix() {
       //  ----------  背景-蓝色底图  ----------
       Matrix.setIdentityM(mBackMatrix, 0);
       // 1.最大位移量
       float maxTransXY = MAX_VISIBLE_SIDE_BACKGROUND - 1f;
       // 2.本次的位移量
       float transX = ((maxTransXY) / MAX_TRANS_DEGREE_Y) * -mCurDegreeY;
       float transY = ((maxTransXY) / MAX_TRANS_DEGREE_X) * -mCurDegreeX;
       float[] backMatrix = new float[16];
       // 蓝色底图的投影矩阵,需要铺展全屏.
       Matrix.setIdentityM(mBgProjectionMatrix, 0);
       Matrix.setIdentityM(backMatrix, 0);
       Matrix.translateM(backMatrix, 0, transX, transY, 0f);                    // 2.平移
       Matrix.scaleM(backMatrix, 0, SCALE_BACK_GROUND, SCALE_BACK_GROUND, 1f);  // 1.缩放
       Matrix.multiplyMM(mBackMatrix, 0, mBgProjectionMatrix, 0, backMatrix, 0);  // 3.正交投影

       //  ----------  背景 -月亮  ----------
       Matrix.setIdentityM(mMoonMatrix, 0);
       float[] midMatrix = new float[16];
       Matrix.setIdentityM(midMatrix, 0);
//        Matrix.translateM(midMatrix, 0, transX, transY, 0f);                      // 2.平移,这行注释解开后,手机摇一摇,封面图和月亮也会有位移偏差.
       Matrix.scaleM(midMatrix, 0, SCALE_MOON_GROUND, SCALE_MOON_GROUND, 1.0f);  // 1.缩放
       Matrix.multiplyMM(mMoonMatrix, 0, mMoonProjectionMatrix, 0, midMatrix, 0);  // 3.正交投影

       // ---------  中景-歌曲封面  ----------
       Matrix.setIdentityM(mCoverMatrix, 0);
       float[] rotateMatrix = new float[16];
       float[] tranAndScale = new float[16];
       float[] coverMatrix = new float[16];

       Matrix.setIdentityM(rotateMatrix, 0);
       Matrix.setIdentityM(tranAndScale, 0);
       Matrix.setIdentityM(coverMatrix, 0);

       Matrix.scaleM(tranAndScale, 0, 0.565f, 0.58f, 1.0f);                   // 3.缩放,这里的缩放参数是开发时,即时调整的,保证歌曲封面和月亮的大小一致
       Matrix.translateM(tranAndScale, 0, 0.05f, 1.41f, 0f);                 // 2.平移,这里的位移参数是开发时,即时调整的,保证歌曲封面和月亮的center位置在一起

       Matrix.setRotateM(rotateMatrix, 0, 360 - mCoverDegree, 0.0f, 0.0f, 1.0f);    // 1.旋转,顺时针

       Matrix.multiplyMM(coverMatrix, 0, tranAndScale, 0, rotateMatrix, 0);
       Matrix.multiplyMM(mCoverMatrix, 0, mCoverProjectionMatrix, 0, coverMatrix, 0);  // 4.正交投影

       //  ----------  前景-装饰  ----------
       Matrix.setIdentityM(mFrontMatrix, 0);
       // 1.最大位移量
       maxTransXY = MAX_VISIBLE_SIDE_FOREGROUND - 1f;
       // 2.本次的位移量
       transX = ((maxTransXY) / MAX_TRANS_DEGREE_Y) * -mCurDegreeY;
       transY = ((maxTransXY) / MAX_TRANS_DEGREE_X) * -mCurDegreeX;
       float[] frontMatrix = new float[16];
       Matrix.setIdentityM(frontMatrix, 0);
       Matrix.translateM(frontMatrix, 0, -transX, -transY, 0f);         // 2.平移
       Matrix.scaleM(frontMatrix, 0, SCALE_FORE_GROUND, SCALE_FORE_GROUND, 1f);    // 1.缩放
       Matrix.multiplyMM(mFrontMatrix, 0, mMoonProjectionMatrix, 0, frontMatrix, 0);  // 3.正交投影
   }
}

背景、月亮、前景都很简单,只有 中景的歌曲封面 麻烦一些,首先歌曲封面要伴着歌曲进度做 旋转动画,其次,由于图片素材尺寸的原因,中心点要 位移 到和月亮相同的位置,最后 缩放 到和月亮一样的大小完成重合。

小结

现在,我们完成了图示效果的开发。

限于篇幅,文中代码以捋清思路为主,部分细节(如 Handler 不断发消息实现旋转动画、添加 低通滤波器 防止抖动等)没展示出来,感兴趣的小伙伴可以点击 这里 查看源码。

关于我

Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 GitHub。

  • 我的Android学习体系
  • 关于文章纠错
  • 关于知识付费
  • 关于《反思》系列

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1030264.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FPGA千兆网 UDP 网络视频传输,基于88E1518 PHY实现,提供工程和QT上位机源码加技术支持

目录 1、前言版本更新说明免责声明 2、我这里已有的以太网方案3、设计思路框架视频源选择OV5640摄像头配置及采集动态彩条UDP协议栈UDP视频数据组包UDP协议栈数据发送UDP协议栈数据缓冲IP地址、端口号的修改Tri Mode Ethernet MAC介绍以及移植注意事项88E1518 PHYQT上位机和源码…

人工智能AI 全栈体系(五)

第一章 神经网络是如何实现的 为什么使用 ReLu 函数&#xff1f; 五、梯度消失问题 1. 什么是梯度消失问题&#xff1f; 前面我们介绍的 BP 算法中&#xff0c;是这样更新权重值的&#xff1a; δ ∗ h o h ( 1 − o h ) ∑ ∗ k ∈ 后续 ( h ) δ ∗ k w ∗ k h \delta*h …

基于Python+Flask实现一个简易网页验证码登录系统案例

在当今的互联网世界中&#xff0c;为了防止恶意访问&#xff0c;许多网站在登录和注册表单中都采用了验证码技术。验证码可以防止机器人自动提交表单&#xff0c;确保提交行为背后有一个真实的人类用户。 本文将向您展示如何使用Python的Flask框架来创建一个简单的验证码登录系…

在线教育线上课堂知识付费源码 网络课堂在线课堂系统源码 含完整代码包和搭建教程

随着互联网技术的不断发展&#xff0c;在线教育逐渐成为了人们获取知识和技能的重要途径。线上课堂作为在线教育的一种具体实现方式&#xff0c;为广大学生提供了便捷、高效的学习平台。 分享一个在线教育线上课堂知识付费源码、网络课堂在线课堂系统源码&#xff0c;含完整搭…

数据分析的-五种常用方法实例

一、对照 俗称对比&#xff0c;单独看一个数据是不会有感觉的&#xff0c;必需跟另一个数据做对比才会有感觉。比如下面的图a和图b。 图a毫无感觉 图b经过跟昨天的成交量对比&#xff0c;就会发现&#xff0c;今天跟昨天实则差了一大截。 这是最基本的思路&#xff0c;也是最重…

数字图像处理中的击中与击不中运算(数字图像处理大题复习 P10)

文章目录 模板元 B 解析尝试覆盖得到结果 击中与击不中 可以看作就是 进阶版的腐蚀 模板元 B 解析 1 就是要求是 1 0 就是要求是 0 x 就是不管&#xff0c;随便是什么 尝试覆盖 如果我们选择一个地方覆盖&#xff0c;他符合这个模板即可 得到结果 这样我们就得到了击中与…

润和软件HopeStage与华宇信息TAS应用中间件完成产品兼容性互认证

近日&#xff0c;江苏润和软件股份有限公司&#xff08;以下简称“润和软件”&#xff09;HopeStage 操作系统与北京华宇信息技术有限公司&#xff08;以下简称“华宇信息”&#xff09;TAS应用中间件软件完成产品兼容性测试。 测试结果表明&#xff0c;企业级通用操作系统Hope…

如何批量为文件夹命名

如果你想要命名一些这样名字具有规律性的文件夹&#xff0c;当文件的数量增多&#xff0c;一个一个命名是非常耗费时间的。很容易想到&#xff0c;如果使用EXCEL&#xff0c;只需往下拉&#xff0c;就能很轻松的拉出1到5。那么&#xff0c;我们如何利用EXCEL来对文件夹进行快速…

高压配电安全监测系统:确保电力系统的稳定运行

随着现代社会对电力需求的不断增长&#xff0c;高压配电系统的重要性日益凸显。为了保证电力系统的稳定运行&#xff0c;提高供电质量&#xff0c;采用高压配电安全监测系统至关重要。 力安科技高压配电安全监测系统通过在每面高压柜&#xff08;进线柜、出线柜、联络柜&#x…

K8S-存储卷,pv,pvc

一、emptyDir存储卷 1.概述 当Pod被分配给节点时&#xff0c;首先创建emptyDir卷&#xff0c;并且只要该Pod在该节点上运行&#xff0c;该卷就会存在。正如卷的名字所述&#xff0c;它最初是空的。Pod 中的容器可以读取和写入emptyDir卷中的相同文件&#xff0c;尽管该卷可以…

React useRequest解读

源码结构&#xff1a; 可以看到虽然是一个hooks&#xff08;具有一定功能且具备状态的单一函数&#xff09; 但是各种文件功能分得也是很细的&#xff0c;方便抽离和复用 useRequest.ts 抽离的原则还是单一功能原则 可以看出 真正的hooks实现是在Implement里 对于类型type的引…

(Clock Domain Crossing)跨时钟域信号的处理 (自我总结)

CummingsSNUG2008Boston_CDC.pdf 参考&#xff1a; 跨时钟域处理方法总结–最终详尽版 - love小酒窝 - 博客园 跨时钟域&#xff08;CDC&#xff09;设计方法之单bit信号篇&#xff08;一&#xff09; | 电子创新网赛灵思社区 孤独的单刀_Verilog语法,FPGA设计与调试,FPGA接口与…

关于ABB机器人的IO创建和设置

首先要链接网线&#xff0c;请求写权限 关于ABB机器人的默认地址位10有的是63.31看你硬接线 ABB机器人分配好信号机器人控制柜要重启 可以把机器人分配的信号导出为EIO 类似与发那科机器人IO

支付宝支付对接-附带完整代码!!

支付宝对接 文章目录 支付宝对接1、大纲1.1 整体业务流程图1.2、开发流程图1.3、核心参数1.4、支付宝开放平台1.5、支付应用场景1.6、支付宝入驻 2、环境准备2.1 首先注册自己的支付宝账号2.2 沙箱环境2.3 支持产品列表 3、项目实现3.1、项目代码地址3.2、 代码层级3.3、快速启…

【产品运营】如何提升B端产品的竞争力(上)

B端产品的核心竞争力不是只有产品功能丰富度、易用度这些维度&#xff0c;判断产品核心竞争力应该基于产品所定位解决的问题场景。 B端产品的成交因素很多&#xff0c;包括产品本身、公司品牌、客情关系、成功案例、产品定价、客户成熟度、需求匹配度等&#xff0c;本文只谈产品…

大数据(九):数据可视化(一)

专栏介绍 结合自身经验和内部资料总结的Python教程&#xff0c;每天3-5章&#xff0c;最短1个月就能全方位的完成Python的学习并进行实战开发&#xff0c;学完了定能成为大佬&#xff01;加油吧&#xff01;卷起来&#xff01; 全部文章请访问专栏&#xff1a;《Python全栈教…

网站整站优化-网站整站优化工具

您是否曾为您的网站在搜索引擎中的排名而感到焦虑&#xff1f;是否苦苦思考如何提高流量、吸引更多用户&#xff1f; 什么是整站优化。简而言之&#xff0c;它是一项用于提升网站在搜索引擎中排名的策略和技巧。通过对网站的内容、结构、速度等方面进行优化&#xff0c;可以使…

Acer宏碁暗影骑士5笔记本AN517-54原装出厂Win10系统工厂模式

宏基电脑原厂WINDOWS10系统自带所有硬件的驱动、NITROSENSE风扇键盘控制中心、Office办公软件、出厂主题壁纸LOGO、 Acer Care Center、Quick Access等预装程序 链接&#xff1a;https://pan.baidu.com/s/1Ovui_CvsUaF-TX0NbuhEVg?pwdcrmv 提取码&#xff1a;crmv 所需要工…

第1讲:前后端分离思想

什么是前端 前端其实是个很大的范畴。 简单点说&#xff0c;针对浏览器的开发&#xff0c;浏览器呈现出来的页面就是前端。它的实质是前端代码在浏览器端被编译、运行、渲染。前端代码主要由三个部分构成&#xff1a;HTML&#xff08;超文本标记语言&#xff09;、CSS&#xf…

[CISCN 2019 初赛]Love Math 通过进制转换执行命令

目录 hex2bin bin2hex base_convert 动态函数 第一种解法 通过get获取参数 绕过 第二种解法 读取请求头 getallheaders echo a,b 第三种解法 异或获得更多字符 这道题也是很有意思&#xff01; 通过规定白名单和黑名单 指定了 函数为数学函数 并且参数也只能是规…